diff --git a/.asf.yaml b/.asf.yaml new file mode 100644 index 000000000..9832b8387 --- /dev/null +++ b/.asf.yaml @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# NOTE: All configurations could be found here: https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features +github: + description: "A Q&A platform software for teams at any scales. Whether it's a community forum, help center, or knowledge management platform, you can always count on Apache Answer." + homepage: https://answer.apache.org + labels: + - react + - go + - golang + - community + - forum + - question + - typescript + - q-and-a + - hacktoberfest + features: + wiki: true + issues: true + projects: true + discussions: false + enabled_merge_buttons: + squash: true + rebase: true + merge: false + protected_branches: + main: {} + +notifications: + commits: commits@answer.apache.org + issues: issues@answer.apache.org + pullrequests: issues@answer.apache.org + discussions: issues@answer.apache.org diff --git a/.editorconfig b/.editorconfig index eff2864de..3fd83d949 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + # http://editorconfig.org root = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..3ac58e3bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Report an issue to help the project improve. +title: '' +labels: bug +type: 'Bug' +assignees: '' + +--- + +## Describe the bug + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Expected behavior + +A clear and concise description of what you expected to happen. + +## Screenshots + +If applicable, add screenshots or video to help explain your problem. + +## Platform + +- Device: [e.g. Desktop, Mobile] +- OS: [e.g. macOS] +- Browser and version: [e.g. Chrome, Safari] +- Version: [e.g. v1.2.0] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..6c4c932ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +blank_issues_enabled: true +contact_links: + - name: Questions & Discussions + url: https://meta.answer.dev + about: If you have any questions while using. diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.md b/.github/ISSUE_TEMPLATE/enhancement_request.md new file mode 100644 index 000000000..7d5d76aa3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.md @@ -0,0 +1,21 @@ +--- +name: Enhancement request +about: Suggest an enhancement for this project. Improve an existing feature. +title: '' +labels: enhancement +type: 'Feature' +assignees: '' + +--- + +## Is your enhancement request related to a problem? Please describe + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..6d9076ef9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea or possible new feature for this project. +title: '' +labels: new-feature +type: 'Feature' +assignees: '' + +--- + +## Is your feature request related to a problem? Please describe + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..4030f6fe7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,7 @@ +Fixes # + +## Proposed Changes + + - + - + - diff --git a/.github/workflows/build-binary-for-release.yml b/.github/workflows/build-binary-for-release.yml new file mode 100644 index 000000000..49c53051a --- /dev/null +++ b/.github/workflows/build-binary-for-release.yml @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Build Binary For Release + +on: + push: + tags: + - "v*" +permissions: + contents: write + +jobs: + build-goreleaser: + if: ${{ github.repository_owner == 'apache' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 20.18.1 + + - name: Node Build + run: make install-ui-packages ui + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: 1.23 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --clean --skip=validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: answer + path: ./dist/* diff --git a/.github/workflows/build-image-for-latest-release.yml b/.github/workflows/build-image-for-latest-release.yml new file mode 100644 index 000000000..b45e28efb --- /dev/null +++ b/.github/workflows/build-image-for-latest-release.yml @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Build Latest Docker Image For Release + +on: + push: + tags: + - v2.* + - v1.* + - v0.* + - "!v*-RC*" + # pull_request: + # branches: [ "main" ] + +jobs: + build: + if: ${{ github.repository_owner == 'apache' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: apache/answer + tags: | + type=raw,value=latest + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + file: ./Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + diff --git a/.github/workflows/build-image-for-manual.yml b/.github/workflows/build-image-for-manual.yml new file mode 100644 index 000000000..4a461b6e1 --- /dev/null +++ b/.github/workflows/build-image-for-manual.yml @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Manual Build Docker Image For Release + +on: + workflow_dispatch: + inputs: + tag_name: + type: string + required: true + description: 'DockerHub img tag name' + +jobs: + build: + if: ${{ github.repository_owner == 'apache' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: apache/answer + tags: | + type=ref,enable=true,priority=600,prefix=,suffix=,event=branch + type=semver,pattern={{version}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + file: ./Dockerfile + tags: apache/answer:${{ inputs.tag_name }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/build-image-for-release.yml b/.github/workflows/build-image-for-release.yml new file mode 100644 index 000000000..fdbf95cdc --- /dev/null +++ b/.github/workflows/build-image-for-release.yml @@ -0,0 +1,68 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Build Docker Image For Release + +on: + push: + tags: + - v2.* + - v1.* + - v0.* + # pull_request: + # branches: [ "main" ] + +jobs: + build: + if: ${{ github.repository_owner == 'apache' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: apache/answer + tags: | + type=ref,enable=true,priority=600,prefix=,suffix=,event=branch + type=semver,pattern={{version}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + file: ./Dockerfile + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + diff --git a/.github/workflows/build-image-for-test.yml b/.github/workflows/build-image-for-test.yml new file mode 100644 index 000000000..c3f438f63 --- /dev/null +++ b/.github/workflows/build-image-for-test.yml @@ -0,0 +1,61 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: Build Docker Image For Test + +on: + push: + branches: [ "test" ] + +jobs: + build: + if: ${{ github.repository_owner == 'apache' }} + name: Build and Push + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: apache/answer + tags: | + type=raw,value=test + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/build_dockerhub_img.yml b/.github/workflows/build_dockerhub_img.yml deleted file mode 100644 index 52e402058..000000000 --- a/.github/workflows/build_dockerhub_img.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build Docker Hub Image - -on: - push: - branches: [ "main" ] - tags: - - 2.* - - 1.* - - 0.* - pull_request: - branches: [ "main" ] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v3 - with: - images: answerdev/answer - tags: | - type=raw,value=latest - # branch event - type=ref,enable=true,priority=600,prefix=,suffix=,event=branch - # tag event - type=ref,enable=true,priority=600,prefix=,suffix=,event=tag - - - - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - diff --git a/.github/workflows/build_github_img.yml b/.github/workflows/build_github_img.yml deleted file mode 100644 index 6a6744755..000000000 --- a/.github/workflows/build_github_img.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build GitHub Image - -on: - push: - branches: [ "main" ] - tags: - - 2.* - - 1.* - - 0.* - pull_request: - branches: [ "main" ] - - -env: - REGISTRY: ghcr.io - IMAGE: answerdev/answer - -jobs: - build-and-push: - runs-on: ubuntu-latest - - permissions: - packages: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Login to ghcr.io - uses: docker/login-action@v1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v3 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE }} - tags: | - type=raw,value=latest - - - - name: Build Img - uses: docker/build-push-action@v3 - with: - context: . - file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - # - name: build to hub.docker - # run: | - # docker build -t answerdev/answer -f ./Dockerfile . - # - name: Login to hub.docker Registry - # run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }} - # - name: Push Image to hub.docker - # run: | - # docker push answerdev/answer - diff --git a/.github/workflows/check-asf-header.yml b/.github/workflows/check-asf-header.yml new file mode 100644 index 000000000..884edaeff --- /dev/null +++ b/.github/workflows/check-asf-header.yml @@ -0,0 +1,46 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: CI +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +# Concurrency strategy: +# github.workflow: distinguish this workflow from others +# github.event_name: distinguish `push` event from `pull_request` event +# github.event.number: set to the number of the pull request if `pull_request` event +# github.run_id: otherwise, it's a `push` or `schedule` event, only cancel if we rerun the workflow +# +# Reference: +# https://docs.github.com/en/actions/using-jobs/using-concurrency +# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} + cancel-in-progress: true + +jobs: + check: + name: Check and lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Check license header + uses: korandoru/hawkeye@v3 diff --git a/.github/workflows/go_build_test.yml b/.github/workflows/go_build_test.yml deleted file mode 100644 index ecd2f4f0b..000000000 --- a/.github/workflows/go_build_test.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Go Build Test - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - - name: Go Test Build - run: make clean build diff --git a/.github/workflows/node_build_test.yml b/.github/workflows/node_build_test.yml deleted file mode 100644 index edf5b8cb2..000000000 --- a/.github/workflows/node_build_test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Node Build Test - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Test Build - run: make install-ui-packages ui diff --git a/.gitignore b/.gitignore index c0aed5e72..1fc116a74 100644 --- a/.gitignore +++ b/.gitignore @@ -3,21 +3,33 @@ *.rej *.so *~ +*.db .DS_Store ._* /.idea /.fleet /.vscode/*.log /cmd/answer/*.sh +/cmd/answer/answer +/cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml /go.work* /logs -/ui/build /ui/node_modules +/ui/build/*/*/* +/ui/build/*.json +/ui/build/*.html +/ui/build/*.txt /vendor Thumbs*.db tmp vendor/ -.husky -answer-data/ +/answer-data/ +/answer +/new_answer + +dist/ + +# Lint setup generated file +.husky/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b9175a7e8..4699b7db2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + include: - project: "segmentfault/devops/templates" file: ".docker-build-push.yml" @@ -5,59 +22,18 @@ include: file: ".deploy-helm.yml" stages: - - compile-html - - compile-golang - - push - deploy-dev -"compile the html and other static files": - image: node:16 - stage: compile-html - tags: - - runner-nanjing - before_script: - - npm config set registry https://repo.huaweicloud.com/repository/npm/ - - make install-ui-packages - script: - - make ui - artifacts: - paths: - - ./build - -"compile the golang project": - image: golang:1.18 - stage: compile-golang - script: - - make generate - - make build - artifacts: - paths: - - ./answer - -"build docker images and push": - stage: push - extends: .docker-build-push - only: - - dev - - master - - main - variables: - DockerNamespace: sf_app - DockerImage: answer - DockerTag: "$CI_COMMIT_SHORT_SHA latest" - DockerfilePath: . - PushPolicy: qingcloud - "deploy-to-local-develop-environment": stage: deploy-dev extends: .deploy-helm only: - - main + - test variables: LoadBalancerIP: 10.0.10.98 KubernetesCluster: dev KubernetesNamespace: "sf-web" - InstallArgs: --set service.loadBalancerIP=${LoadBalancerIP} --set image.tag=$CI_COMMIT_SHORT_SHA --set replicaCount=1 --set serivce.targetPort=80 + InstallArgs: --set service.loadBalancerIP=${LoadBalancerIP} --set image.tag=latest --set replicaCount=1 --set serivce.targetPort=80 ChartName: answer InstallPolicy: replace diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 000000000..c09196937 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +version: 2 + +env: + - GO11MODULE=on + - GO111MODULE=on + - GOPROXY=https://goproxy.io,direct + - CGO_ENABLED=0 + +before: + hooks: + - go mod tidy + +release: + draft: true + +builds: + - id: build + main: ./cmd/answer/. + binary: answer + ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser + flags: -v + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - id: build-windows + main: ./cmd/answer/. + binary: answer + ldflags: -s -w -X github.com/apache/answer/cmd.Version={{.RawVersion}} -X github.com/apache/answer/cmd.Revision={{.ShortCommit}} -X github.com/apache/answer/cmd.Time={{.Date}} -X github.com/apache/answer/cmd.BuildUser=goreleaser + flags: -v + goos: + - windows + goarch: + - amd64 + + + + +archives: + - name_template: >- + apache-answer-{{ .RawVersion }}-bin-{{ .Os }}-{{ .Arch }} + files: + - src: "docs/release/LICENSE" + dst: LICENSE + - src: "docs/release/NOTICE" + dst: NOTICE + - src: "docs/release/licenses/*" + dst: licenses/ + wrap_in_directory: true +checksum: + name_template: 'checksums.txt' +snapshot: + version_template: "{{ incpatch .Version }}" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +source: + enabled: true + name_template: apache-answer-{{ .RawVersion }}-src + prefix_template: "apache-answer-{{ .RawVersion }}-src/" + +# goreleaser release --skip-validate --skip-publish --clean + diff --git a/.vaunt/bug.png b/.vaunt/bug.png new file mode 100644 index 000000000..54b4c64fb Binary files /dev/null and b/.vaunt/bug.png differ diff --git a/.vaunt/config.yaml b/.vaunt/config.yaml new file mode 100644 index 000000000..ec419ee8e --- /dev/null +++ b/.vaunt/config.yaml @@ -0,0 +1,37 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +version: 0.0.1 +achievements: + - achievement: + name: Visionary Architect + icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/enhancement.png + description: Awarded for bringing up enhancement, dream big! + triggers: + - trigger: + actor: assignees + action: issue + condition: labels in ['enhancement'] & labels in ['LGTM'] + - achievement: + name: Bug Hunter + icon: https://raw.githubusercontent.com/apache/answer/main/.vaunt/bug.png + description: Awarded for identifying real bugs, well spotted! + triggers: + - trigger: + actor: assignees + action: issue + condition: labels in ['bug'] & labels in ['LGTM'] diff --git a/.vaunt/enhancement.png b/.vaunt/enhancement.png new file mode 100644 index 000000000..fa6f442a7 Binary files /dev/null and b/.vaunt/enhancement.png differ diff --git a/.vscode/settings.json b/.vscode/settings.json index 93106f189..b7d39e6e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,9 @@ { "eslint.workingDirectories": [ "ui" + ], + "explorer.autoReveal": "focusNoScroll", + "cSpell.words": [ + "grecaptcha" ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3db794774..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,8 +0,0 @@ -# Contributing to answer -## Coding and documentation Style - -To be developed. - -## Submitting Modifications - -To be developed. diff --git a/Dockerfile b/Dockerfile index 4c46698bb..2e90a4301 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,45 +1,74 @@ -FROM node:16 AS node-builder +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -LABEL maintainer="mingcheng" +FROM golang:1.23-alpine AS golang-builder +LABEL maintainer="linkinstar@apache.org" -COPY . /answer -WORKDIR /answer -RUN make install-ui-packages ui && mv ui/build /tmp - -FROM golang:1.18 AS golang-builder -LABEL maintainer="aichy" +ARG GOPROXY +# ENV GOPROXY ${GOPROXY:-direct} +# ENV GOPROXY=https://proxy.golang.com.cn,direct ENV GOPATH /go ENV GOROOT /usr/local/go -ENV PACKAGE github.com/answerdev/answer -ENV GOPROXY https://goproxy.cn,direct +ENV PACKAGE github.com/apache/answer ENV BUILD_DIR ${GOPATH}/src/${PACKAGE} -# Build +ENV ANSWER_MODULE ${BUILD_DIR} + +ARG TAGS="sqlite sqlite_unlock_notify" +ENV TAGS "bindata timetzdata $TAGS" +ARG CGO_EXTRA_CFLAGS + COPY . ${BUILD_DIR} WORKDIR ${BUILD_DIR} -COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build -RUN make clean build && \ - cp answer /usr/bin/answer && \ - mkdir -p /data/upfiles && chmod 777 /data/upfiles && \ - mkdir -p /data/i18n && chmod 777 /data/i18n && cp -r i18n/*.yaml /data/i18n - -FROM debian:bullseye -ENV TZ "Asia/Shanghai" -RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \ - && sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \ - && echo "Asia/Shanghai" > /etc/timezone \ - && apt -y update \ - && apt -y upgrade \ - && apt -y install ca-certificates openssl tzdata curl netcat dumb-init \ - && apt -y autoremove \ - && mkdir -p /tmp/cache +RUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm@9.7.0 \ + && make clean build -COPY --from=golang-builder /data /data -VOLUME /data +RUN chmod 755 answer +RUN ["/bin/bash","-c","script/build_plugin.sh"] +RUN cp answer /usr/bin/answer + +RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ + && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n + +FROM alpine +LABEL maintainer="linkinstar@apache.org" + +ARG TIMEZONE +ENV TIMEZONE=${TIMEZONE:-"Asia/Shanghai"} + +RUN apk update \ + && apk --no-cache add \ + bash \ + ca-certificates \ + curl \ + dumb-init \ + gettext \ + openssh \ + sqlite \ + gnupg \ + tzdata \ + && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ + && echo "${TIMEZONE}" > /etc/timezone COPY --from=golang-builder /usr/bin/answer /usr/bin/answer +COPY --from=golang-builder /data /data COPY /script/entrypoint.sh /entrypoint.sh RUN chmod 755 /entrypoint.sh +VOLUME /data EXPOSE 80 ENTRYPOINT ["/entrypoint.sh"] diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index 0d3969d21..000000000 --- a/INSTALL.md +++ /dev/null @@ -1,103 +0,0 @@ -# How to build and install - -Before installing Answer, you need to install the base environment first. - - database - - [MySQL](http://dev.mysql.com) Version >= 5.7 - -You can then install Answer in several ways: - - - [Deploy with Docker](#Docker-compose-for-Answer) - - [Binary installation](#Install-Answer-using-binary) - - [Source installation](#Compile-the-image) - -## Docker-compose for Answer -```bash -$ mkdir answer && cd answer -$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -$ docker-compose up -``` - -In browser, open URL [http://127.0.0.1:9080/](http://127.0.0.1:9080/). - -You can log in with the default administrator username (**`admin@admin.com`**) and password (**`admin`**). - -## Docker for Answer -Visit [Docker Hub](https://hub.docker.com/r/answerdev/answer) or GitHub Container registry to see all available images and tags. - -### Usage -To persist data beyond the life of a Docker container, use a volume (/var/data -> /data). You can modify this based on your situation. - -``` -# Pull image from Docker Hub. -$ docker pull answerdev/answer:latest - -# Create local directory for volume. -$ mkdir -p /var/data - -# Run the image first -$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer - -# After successful first startup, a configuration file will be generated in the /var/data directory -# /var/data/conf/config.yaml -# Need to modify the Mysql database address in the configuration file -vim /var/data/conf/config.yaml - -# Modify database connection -# connection: [username]:[password]@tcp([host]:[port])/[DbName] -... - -# After configuring the configuration file, you can start the container again to start the service -$ docker start answer -``` - -## Install Answer using binary - - 1. Unzip the compressed package - 2. Use the command `cd` to enter the directory you just created - 3. Execute the command `./answer init` - 4. Answer will generate a `./data` directory in the current directory - 5. Enter the `data` directory and modify the `config.yaml` file - 6. Modify the database connection identify your database connection information - `connection: [username]:[password]@tcp([host]:[port])/[DbName]` - 7. Use `cd ..` to return the directory from step 2, and execute `./answer run -c ./data/conf/config.yaml` - -## Available Commands -Usage: `answer [command]` - -- `help`: Help about any command -- `init`: Init answer application -- `run`: Run answer application -- `check`: Check answer required environment -- `dump`: Backup answer data - -## config.yaml Description -Here is a sample/default config.yaml file, as would be created from `answer init`. -``` -server: - http: - addr: 0.0.0.0:80 #Project access port number -data: - database: - connection: root:root@tcp(127.0.0.1:3306)/answer #MySQL database connection address - cache: - file_path: "/tmp/cache/cache.db" #Cache file storage path -i18n: - bundle_dir: "/data/i18n" #Internationalized file storage directory -swaggerui: - show: true #Whether to display the swaggerapi documentation, address /swagger/index.html - protocol: http #swagger protocol header - host: 127.0.0.1 #An accessible IP address or domain name - address: ':80' #accessible port number -service_config: - secret_key: "answer" #encryption key - web_host: "http://127.0.0.1" #Page access using domain name address - upload_path: "./upfiles" #upload directory -``` - -## Compile the image -If you have modified the source files and want to repackage the image, you can use the following to repackage the image -``` -docker build -t answer:v1.0.0 . -``` -## common problem - 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories. diff --git a/INSTALL_CN.md b/INSTALL_CN.md deleted file mode 100644 index 300851a2e..000000000 --- a/INSTALL_CN.md +++ /dev/null @@ -1,106 +0,0 @@ -# Answer 安装指引 - -安装 Answer 之前,您需要先安装基本环境。 - - 数据库 - - [MySQL](http://dev.mysql.com):版本 >= 5.7 - -然后,您可以通过以下几种方式来安装 Answer: - - - 采用 Docker 部署 - - 二进制安装 - - 源码安装 - -## 使用 Docker-compose 安装 Answer -```bash -$ mkdir answer && cd answer -$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -$ docker-compose up -``` - -启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/). - -你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录. - -## 使用Docker 安装 Answer -可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像 - -### 用法 -将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址 - -``` -# 将镜像从 docker hub 拉到本地 -$ docker pull answerdev/answer:latest - -# 创建一个挂载目录 -$ mkdir -p /var/data - -# 先运行一遍镜像 -$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer - -# 第一次启动后会在/var/data 目录下生成配置文件 -# /var/data/conf/config.yaml -# 需要修改配置文件中的Mysql 数据库地址 -vim /var/data/conf/config.yaml - -# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName] -... - -# 配置好配置文件后可以再次启动镜像即可启动服务 -$ docker start answer -``` - -## 使用二进制 安装 Answer -可以使用编译完成的各个平台的二进制文件运行 Answer 项目 -### 用法 -从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包 - - 1. 解压压缩包 - 2. 使用命令 cd 进入到刚刚创建的目录 - 3. 执行命令 ./answer init - 4. Answer 会在当前目录生成 ./data 目录 - 5. 进入 data 目录修改 config.yaml 文件 - 6. 将数据库连接地址修改为你的数据库连接地址 - - connection: [username]:[password]@tcp([host]:[port])/[DbName] - 7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml - -## 当前支持的命令 -用法: answer [command] - -- help: 帮助 -- init: 初始化环境 -- run: 启动 -- check: 环境依赖检查 -- dump: 备份数据 - -## 配置文件 config.yaml 参数说明 - -``` -server: - http: - addr: 0.0.0.0:80 #项目访问端口号 -data: - database: - connection: root:root@tcp(127.0.0.1:3306)/answer #mysql数据库连接地址 - cache: - file_path: "/tmp/cache/cache.db" #缓存文件存放路径 -i18n: - bundle_dir: "/data/i18n" #国际化文件存放目录 -swaggerui: - show: true #是否显示swaggerapi文档,地址 /swagger/index.html - protocol: http #swagger 协议头 - host: 127.0.0.1 #可被访问的ip地址或域名 - address: ':80' #可被访问的端口号 -service_config: - secret_key: "answer" #加密key - web_host: "http://127.0.0.1" #页面访问使用域名地址 - upload_path: "./upfiles" #上传目录 -``` - -## 编译镜像 -如果修改了源文件并且要重新打包镜像可以使用以下语句重新打包镜像 -``` -docker build -t answer:v1.0.0 . -``` -## 常见问题 - 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项 diff --git a/LICENSE b/LICENSE index 66402cde1..261eeb9e9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 joyqi + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile index 76ae07d8e..0319a0198 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,42 @@ .PHONY: build clean ui -VERSION=0.0.1 +VERSION=1.6.0 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker -#GO_ENV=CGO_ENABLED=0 -Revision=$(shell git rev-parse --short HEAD) -GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X main.Revision=$(Revision) -X 'main.Time=`date`' -extldflags -static" -GO=$(GO_ENV) $(shell which go) +GO_ENV=CGO_ENABLED=0 GO111MODULE=on +Revision=$(shell git rev-parse --short HEAD 2>/dev/null || echo "") +GO_FLAGS=-ldflags="-X github.com/apache/answer/cmd.Version=$(VERSION) -X 'github.com/apache/answer/cmd.Revision=$(Revision)' -X 'github.com/apache/answer/cmd.Time=`date +%s`' -extldflags -static" +GO=$(GO_ENV) "$(shell which go)" -build: - @$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC) +build: generate + @$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC) + +# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3 +universal: generate + @GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC) + @GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC) + @lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64 + @rm -f ${BIN}_amd64 ${BIN}_arm64 generate: - go get github.com/google/wire/cmd/wire@latest - go generate ./... - go mod tidy + @$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3 + @$(GO) get github.com/google/wire/cmd/wire@v0.5.0 + @$(GO) get go.uber.org/mock/mockgen@v0.5.0 + @$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3 + @$(GO) install github.com/google/wire/cmd/wire@v0.5.0 + @$(GO) install go.uber.org/mock/mockgen@v0.5.0 + @$(GO) generate ./... + @$(GO) mod tidy + +check: + @mockgen -version + @swag -v + @wire flags test: - @$(GO) test ./... + @$(GO) test ./internal/repo/repo_test # clean all build result clean: @@ -28,10 +45,13 @@ clean: install-ui-packages: @corepack enable - @corepack prepare pnpm@v7.12.2 --activate + @corepack prepare pnpm@9.7.0 --activate ui: - @npm config set registry https://repo.huaweicloud.com/repository/npm/ - @cd ui && echo "REACT_APP_VERSION=$(VERSION)" >> .env && pnpm install && pnpm build && cd - + @cd ui && pnpm pre-install && pnpm build && cd - + +lint: generate + @bash ./script/check-asf-header.sh + @gofmt -w -l . all: clean build diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..741148fcc --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +Apache Answer +Copyright 2023-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). diff --git a/README.md b/README.md index 921d57bcb..a05b68f81 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ - + logo -# Answer - Build Q&A community +# Apache Answer - Build Q&A platform -An open-source knowledge-based community software. You can use it to quickly build your Q&A community for product technical support, customer support, user communication, and more. +A Q&A platform software for teams at any scales. Whether it’s a community forum, help center, or knowledge management platform, you can always count on Answer. -To learn more about the project, visit [answer.dev](https://answer.dev). +To learn more about the project, visit [answer.apache.org](https://answer.apache.org). -[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE) -[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) -[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/) -[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer) +[![LICENSE](https://img.shields.io/github/license/apache/answer)](https://github.com/apache/answer/blob/main/LICENSE) +[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/) +[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/) +[![Go Report Card](https://goreportcard.com/badge/github.com/apache/answer)](https://goreportcard.com/report/github.com/apache/answer) +[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej) ## Screenshots @@ -19,22 +20,49 @@ To learn more about the project, visit [answer.dev](https://answer.dev). ## Quick start -### Running with docker-compose +### Running with docker ```bash -mkdir answer && cd answer -wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -docker-compose up +docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.6.0 ``` -For more information, see [INSTALL.md](./INSTALL.md) +For more information, see [Installation](https://answer.apache.org/docs/installation). + +### Plugins + +Answer provides a plugin system for developers to create custom plugins and expand Answer’s features. You can find the [plugin documentation here](https://answer.apache.org/community/plugins). + +We value your feedback and suggestions to improve our documentation. If you have any comments or questions, please feel free to contact us. We’re excited to see what you can create using our plugin system! + +You can also check out the [plugins here](https://answer.apache.org/plugins). + +## Building from Source + +### Prerequisites + +- Golang >= 1.23 +- Node.js >= 20 +- pnpm >= 9 +- [mockgen](https://github.com/uber-go/mock?tab=readme-ov-file#installation) >= 1.6.0 +- [wire](https://github.com/google/wire/) >= 0.5.0 + +### Build + +```bash +# Install wire and mockgen for building. You can run `make check` to check if they are installed. +$ make generate +# Install frontend dependencies and build +$ make ui +# Install backend dependencies and build +$ make build +``` ## Contributing Contributions are always welcome! -See [CONTRIBUTING.md](CONTRIBUTING.md) for ways to get started. +See [CONTRIBUTING](https://answer.apache.org/community/contributing) for ways to get started. ## License -[Apache License 2.0](https://github.com/answerdev/answer/blob/main/LICENSE) +[Apache License 2.0](https://github.com/apache/answer/blob/main/LICENSE) diff --git a/README_CN.md b/README_CN.md deleted file mode 100644 index 72b5cffc7..000000000 --- a/README_CN.md +++ /dev/null @@ -1,40 +0,0 @@ - - logo - - -# Answer - 构建问答社区 - -一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。 - -了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev). - -[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE) -[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) -[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/) -[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer) - -## 截图 - -![screenshot](docs/img/screenshot.png) - -## 快速开始 - -### 使用 docker-compose 快速搭建 - -```bash -mkdir answer && cd answer -wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -docker-compose up -``` - -其他安装配置细节请参考 [INSTALL.md](./INSTALL.md) - -## 贡献 - -我们随时欢迎你的贡献! - -参考 [CONTRIBUTING.md](CONTRIBUTING.md) 开始贡献。 - -## License - -[Apache License 2.0](https://github.com/answerdev/answer/blob/main/LICENSE) diff --git a/charts/.helmignore b/charts/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/Chart.yaml b/charts/Chart.yaml index 187c32fcd..ff77995f0 100644 --- a/charts/Chart.yaml +++ b/charts/Chart.yaml @@ -1,6 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + apiVersion: v2 name: answer -description: a simple answer deployments for kubernetes +description: A simple answer deployments for kubernetes type: application version: 0.1.0 -appVersion: "0.1.0" \ No newline at end of file +appVersion: "1.0.7" \ No newline at end of file diff --git a/charts/README.md b/charts/README.md index e4d7d1cc5..26cd6a9cf 100644 --- a/charts/README.md +++ b/charts/README.md @@ -1,2 +1,75 @@ -# Helm Charts for Answer project +# answer +An open-source knowledge-based community software. You can use it quickly to build Q&A community for your products, customers, teams, and more. +## Prerequisites + +- Kubernetes 1.20+ +## Configuration + +The following table lists the configurable parameters of the answer chart and their default values. + +| Parameter | Description | Default | +| --------- | ----------- | ------- | +| `replicaCount` | Number of answer replicas | `1` | +| `image.repository` | Image repository | `apache/answer` | +| `image.pullPolicy` | Image pull policy | `Always` | +| `image.tag` | Image tag | `latest` | +| `env` | Optional environment variables for answer | `LOG_LEVEL: INFO` | +| `extraContainers` | Optional sidecar containers to run along side answer | `[]` | +| `persistence.enabled` | Enable or disable persistence for the /data volume | `true` | +| `persistence.accessMode` | Specify the access mode of the persistent volume | `ReadWriteOnce` | +| `persistence.size` | The size of the persistent volume | `5Gi` | +| `persistence.annotations` | Annotations to add to the volume claim | `{}` | +| `imagePullSecrets` | Reference to one or more secrets to be used when pulling images | `[]` | +| `nameOverride` | nameOverride replaces the name of the chart in the Chart.yaml file, when this is used to construct Kubernetes object names. | | +| `fullnameOverride` | fullnameOverride completely replaces the generated name. | | +| `serviceAccount.create` | If `true`, create a new service account | `true` | +| `serviceAccount.annotations` | Annotations to add to the service account | `{}` | +| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | | +| `podAnnotations` | Annotations to add to the answer pod | `{}` | +| `podSecurityContext` | Security context for the answer pod | `{}` refer to [Default Security Contexts](#default-security-contexts) | +| `securityContext` | Security context for the answer container | `{}` refer to [Default Security Contexts](#default-security-contexts) | +| `service.type` | The type of service to be used | `ClusterIP` | +| `service.port` | The port that the service should listen on for requests. Also used as the container port. | `80` | +| `ingress.enabled` | Enable or disable ingress. | `false` | +| `resources` | CPU/memory resource requests/limits | `{}` | +| `autoscaling.enabled` | Enable or disable pod autoscaling. If enabled, replicas are disabled. | `false` | +| `nodeSelector` | Node labels for pod assignment | `{}` | +| `tolerations` | Node tolerations for pod assignment | `[]` | +| `affinity` | Node affinity for pod assignment | `{}` | + +### Default Security Contexts + +The default pod-level and container-level security contexts, below, adhere to the [restricted](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted) Pod Security Standards policies. + +Default pod-level securityContext: +```yaml +runAsNonRoot: true +seccompProfile: + type: RuntimeDefault +``` + +Default containerSecurityContext: +```yaml +allowPrivilegeEscalation: false +capabilities: + drop: + - ALL +``` +### Installing with a Values file + +```console +$ helm install answer -f values.yaml . +``` +> **Tip**: You can use the default [values.yaml] + +## TODO + +Publish the chart to Artifacthub and add proper installation instructions. E.G. +> **NOTE**: This is not currently a valid installation option. + +```console +$ helm repo add apache https://charts.answer.apache.org/ +$ helm repo update +$ helm install apache/answer -n mynamespace +``` \ No newline at end of file diff --git a/charts/templates/_helpers.tpl b/charts/templates/_helpers.tpl index cfdf84d7e..660cd3300 100644 --- a/charts/templates/_helpers.tpl +++ b/charts/templates/_helpers.tpl @@ -1,5 +1,22 @@ {{/* -Expand the name of the chart. + +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. + */}} {{- define "answer.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} @@ -34,7 +51,7 @@ Create chart name and version as used by the chart label. Common labels */}} {{- define "answer.labels" -}} -helm.sh/chart: {{ .Release.Name }} +helm.sh/chart: {{ include "answer.chart" . }} {{ include "answer.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} @@ -46,6 +63,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} Selector labels */}} {{- define "answer.selectorLabels" -}} -app.kubernetes.io/name: answer +app.kubernetes.io/name: {{ include "answer.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "answer.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "answer.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/templates/config.yaml b/charts/templates/config.yaml deleted file mode 100644 index 24fd733ed..000000000 --- a/charts/templates/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: answer-config - namespace: {{ .Values.namespace | default "default" | quote }} -data: - default.yaml: |- - # diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml new file mode 100644 index 000000000..e71aa836e --- /dev/null +++ b/charts/templates/deployment.yaml @@ -0,0 +1,105 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "answer.fullname" . }} + labels: + {{- include "answer.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "answer.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "answer.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "answer.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.env }} + env: + {{- range .Values.env }} + - name: {{ .name }} + {{- if .value | quote }} + value: {{ .value | quote }} + {{- end }} + {{- if .valueFrom }} + valueFrom: + {{- toYaml .valueFrom | nindent 16 }} + {{- end }} + {{- end }} + {{- end }} + volumeMounts: + - name: data + mountPath: "/data" + {{- if .Values.extraContainers }} + {{- toYaml .Values.extraContainers | nindent 8 }} + {{- end }} + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "answer.fullname" . }}-claim + {{- else }} + emptyDir: {} + {{- end -}} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/charts/templates/hpa.yaml b/charts/templates/hpa.yaml new file mode 100644 index 000000000..fee246ee8 --- /dev/null +++ b/charts/templates/hpa.yaml @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +{{ if .Values.autoscaling.enabled -}} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "answer.fullname" . }} + labels: + {{- include "answer.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "answer.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/templates/ingress.yaml b/charts/templates/ingress.yaml new file mode 100644 index 000000000..c43cf7230 --- /dev/null +++ b/charts/templates/ingress.yaml @@ -0,0 +1,78 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +{{ if .Values.ingress.enabled -}} +{{- $fullName := include "answer.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "answer.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/templates/pvc.yaml b/charts/templates/pvc.yaml new file mode 100644 index 000000000..640fb9fe0 --- /dev/null +++ b/charts/templates/pvc.yaml @@ -0,0 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +{{ if .Values.persistence.enabled -}} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "answer.fullname" . }}-claim + {{- with .Values.persistence.annotations }} + annotations: + {{ toYaml . | indent 4 }} + {{- end }} + labels: + {{- include "answer.labels" . | nindent 4 }} +spec: + {{- if .Values.persistence.storageClass }} + {{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" + {{- end }} + {{- end }} + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- with .Values.persistence.dataSource }} + dataSource: + name: {{ .name }} + kind: {{ .kind | default "VolumeSnapshot" }} + apiGroup: {{ .apiGroup | default "snapshot.storage.k8s.io" }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/templates/service.yaml b/charts/templates/service.yaml index b5db58f8f..653122674 100644 --- a/charts/templates/service.yaml +++ b/charts/templates/service.yaml @@ -1,17 +1,32 @@ ---- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + apiVersion: v1 kind: Service metadata: name: {{ include "answer.fullname" . }} labels: {{- include "answer.labels" . | nindent 4 }} - namespace: {{ .Values.namespace | default "default" | quote }} spec: - type: ClusterIP + type: {{ .Values.service.type }} ports: - - name: answer - port: 80 - targetPort: 80 + - port: {{ .Values.service.port }} + targetPort: http protocol: TCP + name: http selector: {{- include "answer.selectorLabels" . | nindent 4 }} diff --git a/charts/templates/serviceaccount.yaml b/charts/templates/serviceaccount.yaml new file mode 100644 index 000000000..65cc297af --- /dev/null +++ b/charts/templates/serviceaccount.yaml @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +{{ if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "answer.serviceAccountName" . }} + labels: + {{- include "answer.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/templates/statefulset.yaml b/charts/templates/statefulset.yaml deleted file mode 100644 index ac6a6be87..000000000 --- a/charts/templates/statefulset.yaml +++ /dev/null @@ -1,31 +0,0 @@ ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: answer - namespace: {{ .Values.namespace | default "default" | quote }} -spec: - selector: - matchLabels: - {{- include "answer.labels" . | nindent 6 }} - serviceName: answer - replicas: 1 - template: - metadata: - labels: - {{- include "answer.labels" . | nindent 8 }} - spec: - containers: - - name: answer - image: nginx:stable - ports: - - containerPort: 80 - name: answer-ui - volumeMounts: - - name: config - mountPath: "/etc/answer.yaml" - subPath: default.yaml - volumes: - - name: config - configMap: - name: answer-config diff --git a/charts/values.yaml b/charts/values.yaml index 9065ca5b6..d932db848 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -1 +1,170 @@ -namespace: default +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Default values for answer. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: apache/answer + pullPolicy: Always + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +# Environment variables +# Configure environment variables below +# https://answer.apache.org/docs/env +env: + - name: LOG_LEVEL + # [DEBUG INFO WARN ERROR] + value: "INFO" + # uncomment the below values to use AUTO_INSTALL and not have to go through the setup process. + # Once used to do the initial setup, these variables won't be used moving forward. + # You must at a minimum comment AUTO_INSTALL after initial setup to prevent an error about the database already being initiated. + # - name: AUTO_INSTALL + # value: "true" + # - name: DB_TYPE + # value: "sqlite3" + # # DB_FILE Only for sqlite3 + # - name: DB_FILE + # value: "/data/answer.db" + # - name: LANGUAGE + # value: "en-US" + # - name: SITE_NAME + # value: "MyAnswer" + # - name: SITE_URL + # value: "http://localhost:80" + # - name: CONTACT_EMAIL + # value: "support@mydomain.com" + # - name: ADMIN_NAME + # # lowercase + # value: "myadmin" + # - name: ADMIN_PASSWORD + # # 32 Characters MAX + # value: "MyInsecurePasswordInTheRepo!" + # # Use valueFrom to use a secret + # # valueFrom: + # # secretKeyRef: + # # key: answer-admin-password + # # name: answer-secrets + # - name: ADMIN_EMAIL + # value: "myAdmin@mydomain.com" + +# Configure extra containers +extraContainers: [] + # - name: cloudsql-proxy + # image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2 + # command: + # - /cloud-sql-proxy + # args: + # - project:region:instance + # - --port=5432 + # - --auto-iam-authn + # ports: + # - containerPort: 5432 + +# Persistence for the /data volume +# Without persistence, your uploads and config.yaml will not be remembered between restarts. +persistence: + enabled: true + # If set to "-", storageClassName: "", which disables dynamic provisioning + # If undefined (the default) or set to null, no storageClassName spec is + # set, choosing the default provisioner. (gp2 on AWS, standard on + # GKE, AWS & OpenStack) + # storageClass: "-" + accessMode: ReadWriteOnce + size: 5Gi + annotations: {} + # To restore a PVC from a VolumeSnapshot, set the dataSource; + # the kind and apiGroup are optional and default to the shown values + dataSource: {} + # name: my-volume-snapshot + # kind: VolumeSnapshot + # apiGroup: snapshot.storage.k8s.io + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: answer.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: answer-tls + # hosts: + # - answer.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} \ No newline at end of file diff --git a/cmd/answer/command.go b/cmd/answer/command.go deleted file mode 100644 index fd9d8f3bd..000000000 --- a/cmd/answer/command.go +++ /dev/null @@ -1,159 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/answerdev/answer/internal/cli" - "github.com/answerdev/answer/internal/migrations" - "github.com/spf13/cobra" -) - -var ( - // configFilePath is the config file path - configFilePath string - // dataDirPath save all answer application data in this directory. like config file, upload file... - dataDirPath string - // dumpDataPath dump data path - dumpDataPath string -) - -func init() { - rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time) - - initCmd.Flags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") - - rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml") - - dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/") - - for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd} { - rootCmd.AddCommand(cmd) - } -} - -var ( - // rootCmd represents the base command when called without any subcommands - rootCmd = &cobra.Command{ - Use: "answer", - Short: "Answer is a minimalist open source Q&A community.", - Long: `Answer is a minimalist open source Q&A community. -To run answer, use: - - 'answer init' to initialize the required environment. - - 'answer run' to launch application.`, - } - - // runCmd represents the run command - runCmd = &cobra.Command{ - Use: "run", - Short: "Run the application", - Long: `Run the application`, - Run: func(cmd *cobra.Command, args []string) { - runApp() - }, - } - - // initCmd represents the init command - initCmd = &cobra.Command{ - Use: "init", - Short: "init answer application", - Long: `init answer application`, - Run: func(cmd *cobra.Command, args []string) { - cli.InstallAllInitialEnvironment(dataDirPath) - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return - } - fmt.Println("read config successfully") - if err := migrations.InitDB(c.Data.Database); err != nil { - fmt.Println("init database error: ", err.Error()) - return - } - fmt.Println("init database successfully") - }, - } - - // upgradeCmd represents the upgrade command - upgradeCmd = &cobra.Command{ - Use: "upgrade", - Short: "upgrade Answer version", - Long: `upgrade Answer version`, - Run: func(cmd *cobra.Command, args []string) { - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return - } - if err = migrations.Migrate(c.Data.Database); err != nil { - fmt.Println("migrate failed: ", err.Error()) - return - } - fmt.Println("upgrade done") - }, - } - - // dumpCmd represents the dump command - dumpCmd = &cobra.Command{ - Use: "dump", - Short: "back up data", - Long: `back up data`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Answer is backing up data") - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return - } - err = cli.DumpAllData(c.Data.Database, dumpDataPath) - if err != nil { - fmt.Println("dump failed: ", err.Error()) - return - } - fmt.Println("Answer backed up the data successfully.") - }, - } - - // checkCmd represents the check command - checkCmd = &cobra.Command{ - Use: "check", - Short: "checking the required environment", - Long: `Check if the current environment meets the startup requirements`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Start checking the required environment...") - if cli.CheckConfigFile(configFilePath) { - fmt.Println("config file exists [✔]") - } else { - fmt.Println("config file not exists [x]") - } - - if cli.CheckUploadDir() { - fmt.Println("upload directory exists [✔]") - } else { - fmt.Println("upload directory not exists [x]") - } - - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return - } - - if cli.CheckDB(c.Data.Database) { - fmt.Println("db connection successfully [✔]") - } else { - fmt.Println("db connection failed [x]") - } - fmt.Println("check environment all done") - }, - } -) - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 082142440..c11da2ade 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -1,80 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +//go:generate go run github.com/swaggo/swag/cmd/swag init -g ./cmd/answer/main.go -d ../../ -o ../../docs + package main import ( - "os" - "path/filepath" - - "github.com/answerdev/answer/internal/base/conf" - "github.com/answerdev/answer/internal/cli" - "github.com/gin-gonic/gin" - "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/contrib/conf/viper" - "github.com/segmentfault/pacman/contrib/log/zap" - "github.com/segmentfault/pacman/contrib/server/http" - "github.com/segmentfault/pacman/log" -) - -// go build -ldflags "-X main.Version=x.y.z" -var ( - // Name is the name of the project - Name = "answer" - // Version is the version of the project - Version = "development" - // Revision is the git short commit revision number - Revision = "" - // Time is the build time of the project - Time = "" - // log level - logLevel = os.Getenv("LOG_LEVEL") - // log path - logPath = os.Getenv("LOG_PATH") + answercmd "github.com/apache/answer/cmd" ) +// main godoc +// @title Apache Answer +// @description Apache Answer API +// @BasePath / // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization func main() { - Execute() -} - -func runApp() { - log.SetLogger(zap.NewLogger( - log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - - c, err := readConfig() - if err != nil { - panic(err) - } - app, cleanup, err := initApplication( - c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger()) - if err != nil { - panic(err) - } - defer cleanup() - if err := app.Run(); err != nil { - panic(err) - } -} - -func readConfig() (c *conf.AllConfig, err error) { - if len(configFilePath) == 0 { - configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) - } - c = &conf.AllConfig{} - config, err := viper.NewWithPath(configFilePath) - if err != nil { - return nil, err - } - if err = config.Parse(&c); err != nil { - return nil, err - } - return c, nil -} - -func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { - return pacman.NewApp( - pacman.WithName(Name), - pacman.WithVersion(Version), - pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)), - ) + answercmd.Main() } diff --git a/cmd/answer/wire.go b/cmd/answer/wire.go deleted file mode 100644 index 067314f71..000000000 --- a/cmd/answer/wire.go +++ /dev/null @@ -1,46 +0,0 @@ -//go:build wireinject -// +build wireinject - -// The build tag makes sure the stub is not built in the final build. - -package main - -import ( - "github.com/answerdev/answer/internal/base/conf" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/server" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/controller" - "github.com/answerdev/answer/internal/controller_backyard" - "github.com/answerdev/answer/internal/repo" - "github.com/answerdev/answer/internal/router" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/service_config" - "github.com/google/wire" - "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/log" -) - -// initApplication init application. -func initApplication( - debug bool, - serverConf *conf.Server, - dbConf *data.Database, - cacheConf *data.CacheConf, - i18nConf *translator.I18n, - swaggerConf *router.SwaggerConfig, - serviceConf *service_config.ServiceConfig, - logConf log.Logger) (*pacman.Application, func(), error) { - panic(wire.Build( - server.ProviderSetServer, - router.ProviderSetRouter, - controller.ProviderSetController, - controller_backyard.ProviderSetController, - service.ProviderSetService, - repo.ProviderSetRepo, - translator.ProviderSet, - middleware.ProviderSetMiddleware, - newApplication, - )) -} diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go deleted file mode 100644 index cc83240d0..000000000 --- a/cmd/answer/wire_gen.go +++ /dev/null @@ -1,183 +0,0 @@ -// Code generated by Wire. DO NOT EDIT. - -//go:generate go run github.com/google/wire/cmd/wire -//go:build !wireinject -// +build !wireinject - -package main - -import ( - "github.com/answerdev/answer/internal/base/conf" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/server" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/controller" - "github.com/answerdev/answer/internal/controller_backyard" - "github.com/answerdev/answer/internal/repo" - "github.com/answerdev/answer/internal/repo/activity" - "github.com/answerdev/answer/internal/repo/activity_common" - "github.com/answerdev/answer/internal/repo/auth" - "github.com/answerdev/answer/internal/repo/captcha" - "github.com/answerdev/answer/internal/repo/collection" - "github.com/answerdev/answer/internal/repo/comment" - "github.com/answerdev/answer/internal/repo/common" - "github.com/answerdev/answer/internal/repo/config" - "github.com/answerdev/answer/internal/repo/export" - "github.com/answerdev/answer/internal/repo/meta" - "github.com/answerdev/answer/internal/repo/notification" - "github.com/answerdev/answer/internal/repo/rank" - "github.com/answerdev/answer/internal/repo/reason" - "github.com/answerdev/answer/internal/repo/report" - "github.com/answerdev/answer/internal/repo/revision" - "github.com/answerdev/answer/internal/repo/tag" - "github.com/answerdev/answer/internal/repo/unique" - "github.com/answerdev/answer/internal/repo/user" - "github.com/answerdev/answer/internal/router" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/action" - activity2 "github.com/answerdev/answer/internal/service/activity" - "github.com/answerdev/answer/internal/service/answer_common" - auth2 "github.com/answerdev/answer/internal/service/auth" - "github.com/answerdev/answer/internal/service/collection_common" - comment2 "github.com/answerdev/answer/internal/service/comment" - export2 "github.com/answerdev/answer/internal/service/export" - "github.com/answerdev/answer/internal/service/follow" - meta2 "github.com/answerdev/answer/internal/service/meta" - notification2 "github.com/answerdev/answer/internal/service/notification" - "github.com/answerdev/answer/internal/service/notification_common" - "github.com/answerdev/answer/internal/service/object_info" - "github.com/answerdev/answer/internal/service/question_common" - rank2 "github.com/answerdev/answer/internal/service/rank" - reason2 "github.com/answerdev/answer/internal/service/reason" - report2 "github.com/answerdev/answer/internal/service/report" - "github.com/answerdev/answer/internal/service/report_backyard" - "github.com/answerdev/answer/internal/service/report_handle_backyard" - "github.com/answerdev/answer/internal/service/revision_common" - "github.com/answerdev/answer/internal/service/service_config" - tag2 "github.com/answerdev/answer/internal/service/tag" - "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/internal/service/uploader" - "github.com/answerdev/answer/internal/service/user_backyard" - "github.com/answerdev/answer/internal/service/user_common" - "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/log" -) - -// Injectors from wire.go: - -// initApplication init application. -func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, logConf log.Logger) (*pacman.Application, func(), error) { - staticRouter := router.NewStaticRouter(serviceConf) - i18nTranslator, err := translator.NewTranslator(i18nConf) - if err != nil { - return nil, nil, err - } - langController := controller.NewLangController(i18nTranslator) - engine, err := data.NewDB(debug, dbConf) - if err != nil { - return nil, nil, err - } - cache, cleanup, err := data.NewCache(cacheConf) - if err != nil { - return nil, nil, err - } - dataData, cleanup2, err := data.NewData(engine, cache) - if err != nil { - cleanup() - return nil, nil, err - } - authRepo := auth.NewAuthRepo(dataData) - authService := auth2.NewAuthService(authRepo) - configRepo := config.NewConfigRepo(dataData) - userRepo := user.NewUserRepo(dataData, configRepo) - uniqueIDRepo := unique.NewUniqueIDRepo(dataData) - activityRepo := repo.NewActivityRepo(dataData, uniqueIDRepo, configRepo) - userRankRepo := rank.NewUserRankRepo(dataData, configRepo) - userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) - emailRepo := export.NewEmailRepo(dataData) - siteInfoRepo := repo.NewSiteInfo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) - captchaRepo := captcha.NewCaptchaRepo(dataData) - captchaService := action.NewCaptchaService(captchaRepo) - uploaderService := uploader.NewUploaderService(serviceConf) - userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) - commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) - commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) - userCommon := usercommon.NewUserCommon(userRepo) - answerRepo := repo.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) - questionRepo := repo.NewQuestionRepo(dataData, uniqueIDRepo) - tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) - objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo) - voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) - commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo) - rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo) - commentController := controller.NewCommentController(commentService, rankService) - reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) - reportService := report2.NewReportService(reportRepo, objService) - reportController := controller.NewReportController(reportService, rankService) - serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo) - voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService) - voteController := controller.NewVoteController(voteService) - revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) - revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) - followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) - tagService := tag2.NewTagService(tagRepo, revisionService, followRepo) - tagController := controller.NewTagController(tagService, rankService) - followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) - followService := follow.NewFollowService(followFollowRepo, followRepo, tagRepo) - followController := controller.NewFollowController(followService) - collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) - collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) - tagRelRepo := tag.NewTagListRepo(dataData) - tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService) - collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) - answerCommon := answercommon.NewAnswerCommon(answerRepo) - metaRepo := meta.NewMetaRepo(dataData) - metaService := meta2.NewMetaService(metaRepo) - questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configRepo) - collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) - collectionController := controller.NewCollectionController(collectionService) - answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo) - questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo) - answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo) - questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) - questionController := controller.NewQuestionController(questionService, rankService) - answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - answerController := controller.NewAnswerController(answerService, rankService) - searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userCommon) - searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) - searchController := controller.NewSearchController(searchService) - serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) - revisionController := controller.NewRevisionController(serviceRevisionService) - rankController := controller.NewRankController(rankService) - commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo) - reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo) - reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) - controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService) - userBackyardRepo := user.NewUserBackyardRepo(dataData) - userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo) - userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService) - reasonRepo := reason.NewReasonRepo(configRepo) - reasonService := reason2.NewReasonService(reasonRepo) - reasonController := controller.NewReasonController(reasonService) - themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) - siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) - notificationRepo := notification.NewNotificationRepo(dataData) - notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) - notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) - notificationController := controller.NewNotificationController(notificationService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController) - swaggerRouter := router.NewSwaggerRouter(swaggerConf) - uiRouter := router.NewUIRouter() - authUserMiddleware := middleware.NewAuthUserMiddleware(authService) - ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware) - application := newApplication(serverConf, ginEngine) - return application, func() { - cleanup2() - cleanup() - }, nil -} diff --git a/cmd/command.go b/cmd/command.go new file mode 100644 index 000000000..7d779893f --- /dev/null +++ b/cmd/command.go @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package answercmd + +import ( + "fmt" + "os" + "strings" + + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/internal/cli" + "github.com/apache/answer/internal/install" + "github.com/apache/answer/internal/migrations" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" + "github.com/spf13/cobra" +) + +var ( + // dataDirPath save all answer application data in this directory. like config file, upload file... + dataDirPath string + // dumpDataPath dump data path + dumpDataPath string + // place to build new answer + buildDir string + // plugins needed to build in answer application + buildWithPlugins []string + // build output path + buildOutput string + // This config is used to upgrade the database from a specific version manually. + // If you want to upgrade the database to version 1.1.0, you can use `answer upgrade -f v1.1.0`. + upgradeVersion string + // The fields that need to be set to the default value + configFields []string + // i18nSourcePath i18n from path + i18nSourcePath string + // i18nTargetPath i18n to path + i18nTargetPath string +) + +func init() { + rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time) + + rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") + + dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/") + + buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build") + + buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path") + + buildCmd.Flags().StringVarP(&buildDir, "build-dir", "b", "", "dir for build process") + + upgradeCmd.Flags().StringVarP(&upgradeVersion, "from", "f", "", "upgrade from specific version, eg: -f v1.1.0") + + configCmd.Flags().StringSliceVarP(&configFields, "with", "w", []string{}, "the fields that need to be set to the default value, eg: -w allow_password_login") + + i18nCmd.Flags().StringVarP(&i18nSourcePath, "source", "s", "", "i18n source path, eg: -s ./i18n/source") + + i18nCmd.Flags().StringVarP(&i18nTargetPath, "target", "t", "", "i18n target path, eg: -t ./i18n/target") + + for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd} { + rootCmd.AddCommand(cmd) + } +} + +var ( + rootCmd = &cobra.Command{ + Use: "answer", + Short: "Answer is a minimalist open source Q&A community.", + Long: `Answer is a minimalist open source Q&A community. +To run answer, use: + - 'answer init' to initialize the required environment. + - 'answer run' to launch application.`, + } + + runCmd = &cobra.Command{ + Use: "run", + Short: "Run Answer", + Long: `Start running Answer`, + Run: func(_ *cobra.Command, _ []string) { + cli.FormatAllPath(dataDirPath) + fmt.Println("config file path: ", cli.GetConfigFilePath()) + fmt.Println("Answer is starting..........................") + runApp() + }, + } + + initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize Answer", + Long: `Initialize Answer with specified configuration`, + Run: func(_ *cobra.Command, _ []string) { + // check config file and database. if config file exists and database is already created, init done + cli.InstallAllInitialEnvironment(dataDirPath) + + configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath()) + if configFileExist { + fmt.Println("config file exists, try to read the config...") + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + fmt.Println("config file read successfully, try to connect database...") + if cli.CheckDBTableExist(c.Data.Database) { + fmt.Println("connect to database successfully and table already exists, do nothing.") + return + } + } + + // start installation server to install + install.Run(cli.GetConfigFilePath()) + }, + } + + upgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "Upgrade Answer", + Long: `Upgrade Answer to the latest version`, + Run: func(_ *cobra.Command, _ []string) { + log.SetLogger(log.NewStdLogger(os.Stdout)) + cli.FormatAllPath(dataDirPath) + cli.InstallI18nBundle(true) + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + if err = migrations.Migrate(c.Debug, c.Data.Database, c.Data.Cache, upgradeVersion); err != nil { + fmt.Println("migrate failed: ", err.Error()) + return + } + fmt.Println("upgrade done") + }, + } + + dumpCmd = &cobra.Command{ + Use: "dump", + Short: "Back up data", + Long: `Back up database into an SQL file`, + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("Answer is backing up data") + cli.FormatAllPath(dataDirPath) + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + err = cli.DumpAllData(c.Data.Database, dumpDataPath) + if err != nil { + fmt.Println("dump failed: ", err.Error()) + return + } + fmt.Println("Answer backed up the data successfully.") + }, + } + + checkCmd = &cobra.Command{ + Use: "check", + Short: "Check the required environment", + Long: `Check if the current environment meets the startup requirements`, + Run: func(_ *cobra.Command, _ []string) { + cli.FormatAllPath(dataDirPath) + fmt.Println("Start checking the required environment...") + if cli.CheckConfigFile(cli.GetConfigFilePath()) { + fmt.Println("config file exists [✔]") + } else { + fmt.Println("config file not exists [x]") + } + + if cli.CheckUploadDir() { + fmt.Println("upload directory exists [✔]") + } else { + fmt.Println("upload directory not exists [x]") + } + + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + if cli.CheckDBConnection(c.Data.Database) { + fmt.Println("db connection successfully [✔]") + } else { + fmt.Println("db connection failed [x]") + } + fmt.Println("check environment all done") + }, + } + + buildCmd = &cobra.Command{ + Use: "build", + Short: "Build Answer with plugins", + Long: `Build a new Answer with plugins that you need`, + Run: func(_ *cobra.Command, _ []string) { + fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n")) + err := cli.BuildNewAnswer(buildDir, buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{ + Version: Version, + Revision: Revision, + Time: Time, + }) + if err != nil { + fmt.Printf("build failed %v\n", err) + os.Exit(1) + } + + fmt.Printf("build new answer successfully %s\n", buildOutput) + }, + } + + pluginCmd = &cobra.Command{ + Use: "plugin", + Short: "Print all plugins packed in the binary", + Long: `Print all plugins packed in the binary`, + Run: func(_ *cobra.Command, _ []string) { + _ = plugin.CallBase(func(base plugin.Base) error { + info := base.Info() + fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author) + return nil + }) + }, + } + + configCmd = &cobra.Command{ + Use: "config", + Short: "Set some config to default value", + Long: `Set some config to default value`, + Run: func(_ *cobra.Command, _ []string) { + cli.FormatAllPath(dataDirPath) + + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + field := &cli.ConfigField{} + fmt.Println(configFields) + if len(configFields) > 0 { + switch configFields[0] { + case "allow_password_login": + field.AllowPasswordLogin = true + case "deactivate_plugin": + if len(configFields) > 1 { + field.DeactivatePluginSlugName = configFields[1] + } + default: + fmt.Printf("field %s not support\n", configFields[0]) + } + } + err = cli.SetDefaultConfig(c.Data.Database, c.Data.Cache, field) + if err != nil { + fmt.Println("set default config failed: ", err.Error()) + } else { + fmt.Println("set default config successfully") + } + }, + } + + i18nCmd = &cobra.Command{ + Use: "i18n", + Short: "Overwrite i18n files", + Long: `Merge i18n files from plugins to original i18n files. It will overwrite the original i18n files`, + Run: func(_ *cobra.Command, _ []string) { + if err := cli.ReplaceI18nFilesLocal(i18nTargetPath); err != nil { + fmt.Printf("replace i18n files failed %v\n", err) + } else { + fmt.Printf("replace i18n files successfully\n") + } + + fmt.Printf("try to merge i18n files from %q to %q\n", i18nSourcePath, i18nTargetPath) + + if err := cli.MergeI18nFilesLocal(i18nTargetPath, i18nSourcePath); err != nil { + fmt.Printf("merge i18n files failed %v\n", err) + } else { + fmt.Printf("merge i18n files successfully\n") + } + }, + } +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000..35256c073 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package answercmd + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/cron" + "github.com/apache/answer/internal/cli" + "github.com/apache/answer/internal/schema" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman" + "github.com/segmentfault/pacman/contrib/log/zap" + "github.com/segmentfault/pacman/contrib/server/http" + "github.com/segmentfault/pacman/log" +) + +// go build -ldflags "-X github.com/apache/answer/cmd.Version=x.y.z" +var ( + // Name is the name of the project + Name = "answer" + // Version is the version of the project + Version = "0.0.0" + // Revision is the git short commit revision number + // If built without a Git repository, this field will be empty. + Revision = "" + // Time is the build time of the project + Time = "" + // GoVersion is the go version of the project + GoVersion = "1.23" + // log level + logLevel = os.Getenv("LOG_LEVEL") + // log path + logPath = os.Getenv("LOG_PATH") +) + +// Main +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name Authorization +func Main() { + log.SetLogger(zap.NewLogger( + log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath))) + Execute() +} + +func runApp() { + c, err := conf.ReadConfig(cli.GetConfigFilePath()) + if err != nil { + panic(err) + } + app, cleanup, err := initApplication( + c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, c.UI, log.GetLogger()) + if err != nil { + panic(err) + } + constant.Version = Version + constant.Revision = Revision + constant.GoVersion = GoVersion + schema.AppStartTime = time.Now() + fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision) + + defer cleanup() + if err := app.Run(context.Background()); err != nil { + panic(err) + } +} + +func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application { + manager.Run() + return pacman.NewApp( + pacman.WithName(Name), + pacman.WithVersion(Version), + pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)), + ) +} diff --git a/cmd/wire.go b/cmd/wire.go new file mode 100644 index 000000000..b25026e5b --- /dev/null +++ b/cmd/wire.go @@ -0,0 +1,70 @@ +//go:build wireinject +// +build wireinject + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// The build tag makes sure the stub is not built in the final build. + +package answercmd + +import ( + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/internal/base/cron" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/server" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/controller" + "github.com/apache/answer/internal/controller/template_render" + "github.com/apache/answer/internal/controller_admin" + "github.com/apache/answer/internal/repo" + "github.com/apache/answer/internal/router" + "github.com/apache/answer/internal/service" + "github.com/apache/answer/internal/service/service_config" + "github.com/google/wire" + "github.com/segmentfault/pacman" + "github.com/segmentfault/pacman/log" +) + +// initApplication init application. +func initApplication( + debug bool, + serverConf *conf.Server, + dbConf *data.Database, + cacheConf *data.CacheConf, + i18nConf *translator.I18n, + swaggerConf *router.SwaggerConfig, + serviceConf *service_config.ServiceConfig, + uiConf *server.UI, + logConf log.Logger) (*pacman.Application, func(), error) { + panic(wire.Build( + server.ProviderSetServer, + router.ProviderSetRouter, + controller.ProviderSetController, + controller_admin.ProviderSetController, + templaterender.ProviderSetTemplateRenderController, + service.ProviderSetService, + cron.ProviderSetService, + repo.ProviderSetRepo, + translator.ProviderSet, + middleware.ProviderSetMiddleware, + newApplication, + )) +} diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go new file mode 100644 index 000000000..947b6605d --- /dev/null +++ b/cmd/wire_gen.go @@ -0,0 +1,300 @@ +//go:build !wireinject +// +build !wireinject + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire + +package answercmd + +import ( + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/internal/base/cron" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/server" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/controller" + "github.com/apache/answer/internal/controller/template_render" + "github.com/apache/answer/internal/controller_admin" + "github.com/apache/answer/internal/repo/activity" + "github.com/apache/answer/internal/repo/activity_common" + "github.com/apache/answer/internal/repo/answer" + "github.com/apache/answer/internal/repo/auth" + "github.com/apache/answer/internal/repo/badge" + "github.com/apache/answer/internal/repo/badge_award" + "github.com/apache/answer/internal/repo/badge_group" + "github.com/apache/answer/internal/repo/captcha" + "github.com/apache/answer/internal/repo/collection" + "github.com/apache/answer/internal/repo/comment" + "github.com/apache/answer/internal/repo/config" + "github.com/apache/answer/internal/repo/export" + "github.com/apache/answer/internal/repo/file_record" + "github.com/apache/answer/internal/repo/limit" + "github.com/apache/answer/internal/repo/meta" + notification2 "github.com/apache/answer/internal/repo/notification" + "github.com/apache/answer/internal/repo/plugin_config" + "github.com/apache/answer/internal/repo/question" + "github.com/apache/answer/internal/repo/rank" + "github.com/apache/answer/internal/repo/reason" + "github.com/apache/answer/internal/repo/report" + "github.com/apache/answer/internal/repo/review" + "github.com/apache/answer/internal/repo/revision" + "github.com/apache/answer/internal/repo/role" + "github.com/apache/answer/internal/repo/search_common" + "github.com/apache/answer/internal/repo/site_info" + "github.com/apache/answer/internal/repo/tag" + "github.com/apache/answer/internal/repo/tag_common" + "github.com/apache/answer/internal/repo/unique" + "github.com/apache/answer/internal/repo/user" + "github.com/apache/answer/internal/repo/user_external_login" + "github.com/apache/answer/internal/repo/user_notification_config" + "github.com/apache/answer/internal/router" + "github.com/apache/answer/internal/service/action" + activity2 "github.com/apache/answer/internal/service/activity" + activity_common2 "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/internal/service/answer_common" + auth2 "github.com/apache/answer/internal/service/auth" + badge2 "github.com/apache/answer/internal/service/badge" + collection2 "github.com/apache/answer/internal/service/collection" + "github.com/apache/answer/internal/service/collection_common" + comment2 "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/comment_common" + config2 "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/dashboard" + "github.com/apache/answer/internal/service/event_queue" + export2 "github.com/apache/answer/internal/service/export" + file_record2 "github.com/apache/answer/internal/service/file_record" + "github.com/apache/answer/internal/service/follow" + "github.com/apache/answer/internal/service/importer" + meta2 "github.com/apache/answer/internal/service/meta" + "github.com/apache/answer/internal/service/meta_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/notification" + "github.com/apache/answer/internal/service/notification_common" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/internal/service/plugin_common" + "github.com/apache/answer/internal/service/question_common" + rank2 "github.com/apache/answer/internal/service/rank" + reason2 "github.com/apache/answer/internal/service/reason" + report2 "github.com/apache/answer/internal/service/report" + "github.com/apache/answer/internal/service/report_handle" + review2 "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision_common" + role2 "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/search_parser" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/siteinfo" + "github.com/apache/answer/internal/service/siteinfo_common" + tag2 "github.com/apache/answer/internal/service/tag" + tag_common2 "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/internal/service/uploader" + "github.com/apache/answer/internal/service/user_admin" + "github.com/apache/answer/internal/service/user_common" + user_external_login2 "github.com/apache/answer/internal/service/user_external_login" + user_notification_config2 "github.com/apache/answer/internal/service/user_notification_config" + "github.com/segmentfault/pacman" + "github.com/segmentfault/pacman/log" +) + +// Injectors from wire.go: + +// initApplication init application. +func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, uiConf *server.UI, logConf log.Logger) (*pacman.Application, func(), error) { + staticRouter := router.NewStaticRouter(serviceConf) + i18nTranslator, err := translator.NewTranslator(i18nConf) + if err != nil { + return nil, nil, err + } + engine, err := data.NewDB(debug, dbConf) + if err != nil { + return nil, nil, err + } + cache, cleanup, err := data.NewCache(cacheConf) + if err != nil { + return nil, nil, err + } + dataData, cleanup2, err := data.NewData(engine, cache) + if err != nil { + cleanup() + return nil, nil, err + } + siteInfoRepo := site_info.NewSiteInfo(dataData) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) + authRepo := auth.NewAuthRepo(dataData) + authService := auth2.NewAuthService(authRepo) + userRepo := user.NewUserRepo(dataData) + uniqueIDRepo := unique.NewUniqueIDRepo(dataData) + configRepo := config.NewConfigRepo(dataData) + configService := config2.NewConfigService(configRepo) + activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configService) + userRankRepo := rank.NewUserRankRepo(dataData, configService) + userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configService) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configService, emailRepo, siteInfoCommonService) + userRoleRelRepo := role.NewUserRoleRelRepo(dataData) + roleRepo := role.NewRoleRepo(dataData) + roleService := role2.NewRoleService(roleRepo) + userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService) + userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService, siteInfoCommonService) + userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData) + userNotificationConfigRepo := user_notification_config.NewUserNotificationConfigRepo(dataData) + userNotificationConfigService := user_notification_config2.NewUserNotificationConfigService(userRepo, userNotificationConfigRepo) + userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo, userNotificationConfigService) + questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo) + answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) + voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) + followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) + tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo) + tagRelRepo := tag.NewTagRelRepo(dataData, uniqueIDRepo) + tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo) + revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) + revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) + activityQueueService := activity_queue.NewActivityQueueService() + tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, activityQueueService) + collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) + collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) + answerCommon := answercommon.NewAnswerCommon(answerRepo) + metaRepo := meta.NewMetaRepo(dataData) + metaCommonService := metacommon.NewMetaCommonService(metaRepo) + questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, siteInfoCommonService, dataData) + eventQueueService := event_queue.NewEventQueueService() + fileRecordRepo := file_record.NewFileRecordRepo(dataData) + fileRecordService := file_record2.NewFileRecordService(fileRecordRepo, revisionRepo, serviceConf, siteInfoCommonService, userCommon) + userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventQueueService, fileRecordService) + captchaRepo := captcha.NewCaptchaRepo(dataData) + captchaService := action.NewCaptchaService(captchaRepo) + userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService) + commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) + commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) + objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) + notificationQueueService := notice_queue.NewNotificationQueueService() + externalNotificationQueueService := notice_queue.NewNewQuestionNotificationQueueService() + commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService, eventQueueService) + rolePowerRelRepo := role.NewRolePowerRelRepo(dataData) + rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService) + rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService) + limitRepo := limit.NewRateLimitRepo(dataData) + rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo) + commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware) + reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) + tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) + answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) + externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService) + reviewRepo := review.NewReviewRepo(dataData) + reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService) + questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService, reviewRepo) + answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService, eventQueueService) + reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) + reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventQueueService) + reportController := controller.NewReportController(reportService, rankService, captchaService) + contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) + voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService, eventQueueService) + voteController := controller.NewVoteController(voteService, rankService, captchaService) + tagController := controller.NewTagController(tagService, tagCommonService, rankService) + followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) + followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) + followController := controller.NewFollowController(followService) + collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) + collectionService := collection2.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon) + collectionController := controller.NewCollectionController(collectionService) + questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware) + answerController := controller.NewAnswerController(answerService, rankService, captchaService, siteInfoCommonService, rateLimitMiddleware) + searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) + searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon, tagCommonService) + searchService := content.NewSearchService(searchParser, searchRepo) + searchController := controller.NewSearchController(searchService, captchaService) + reviewActivityRepo := activity.NewReviewActivityRepo(dataData, activityRepo, userRankRepo, configService) + contentRevisionService := content.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, notificationQueueService, activityQueueService, reportRepo, reviewService, reviewActivityRepo) + revisionController := controller.NewRevisionController(contentRevisionService, rankService) + rankController := controller.NewRankController(rankService) + userAdminRepo := user.NewUserAdminRepo(dataData, authRepo) + notificationRepo := notification2.NewNotificationRepo(dataData) + pluginUserConfigRepo := plugin_config.NewPluginUserConfigRepo(dataData) + badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) + userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo, notificationRepo, pluginUserConfigRepo, badgeAwardRepo) + userAdminController := controller_admin.NewUserAdminController(userAdminService) + reasonRepo := reason.NewReasonRepo(configService) + reasonService := reason2.NewReasonService(reasonRepo) + reasonController := controller.NewReasonController(reasonService) + themeController := controller_admin.NewThemeController() + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon, fileRecordService) + siteInfoController := controller_admin.NewSiteInfoController(siteInfoService) + controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) + notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService, userExternalLoginRepo, siteInfoCommonService) + badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) + notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService, badgeRepo) + notificationController := controller.NewNotificationController(notificationService, rankService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData) + dashboardController := controller.NewDashboardController(dashboardService) + uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService, fileRecordService) + uploadController := controller.NewUploadController(uploaderService) + activityActivityRepo := activity.NewActivityRepo(dataData, configService) + activityCommon := activity_common2.NewActivityCommon(activityRepo, activityQueueService) + commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo) + activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaCommonService, configService) + activityController := controller.NewActivityController(activityService) + roleController := controller_admin.NewRoleController(roleService) + pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData) + importerService := importer.NewImporterService(questionService, rankService, userCommon) + pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, pluginUserConfigRepo, configService, dataData, importerService) + pluginController := controller_admin.NewPluginController(pluginCommonService) + permissionController := controller.NewPermissionController(rankService) + userPluginController := controller.NewUserPluginController(pluginCommonService) + reviewController := controller.NewReviewController(reviewService, rankService, captchaService) + metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventQueueService) + metaController := controller.NewMetaController(metaService) + badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) + eventRuleRepo := badge.NewEventRuleRepo(dataData) + badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService, notificationQueueService) + badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo, badgeAwardService) + badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService, siteInfoCommonService) + badgeController := controller.NewBadgeController(badgeService, badgeAwardService) + controller_adminBadgeController := controller_admin.NewBadgeController(badgeService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController) + swaggerRouter := router.NewSwaggerRouter(swaggerConf) + uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) + authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) + avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) + shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService) + templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo) + templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventQueueService, userService, questionService) + templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware) + connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService) + userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService) + userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService) + captchaController := controller.NewCaptchaController() + embedController := controller.NewEmbedController() + renderController := controller.NewRenderController() + pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController) + ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter, uiConf) + scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService, fileRecordService, userAdminService, serviceConf) + application := newApplication(serverConf, ginEngine, scheduledTaskManager) + return application, func() { + cleanup2() + cleanup() + }, nil +} diff --git a/configs/config.go b/configs/config.go index 52c7b8439..d6c1b509b 100644 --- a/configs/config.go +++ b/configs/config.go @@ -1,6 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package configs import _ "embed" //go:embed config.yaml var Config []byte + +//go:embed path_ignore.yaml +var PathIgnore []byte + +//go:embed reserved-usernames.json +var ReservedUsernames []byte diff --git a/configs/config.yaml b/configs/config.yaml index 3e7cfd84b..d14072785 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -1,12 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + server: http: addr: 0.0.0.0:80 data: database: - driver: "mysql" - connection: root:root@tcp(db:3306)/answer + driver: "sqlite3" + connection: "/data/sqlite3/answer.db" cache: - file_path: "/tmp/cache/cache.db" + file_path: "/data/cache/cache.db" i18n: bundle_dir: "/data/i18n" swaggerui: @@ -15,6 +32,13 @@ swaggerui: host: 127.0.0.1 address: ':80' service_config: - secret_key: "answer" - web_host: "http://127.0.0.1:9080" - upload_path: "/data/upfiles" + upload_path: "/data/uploads" + clean_up_uploads: true + clean_orphan_uploads_period_hours: 48 + purge_deleted_files_period_days: 30 +ui: + public_url: '/' + api_url: '/' + base_url: '' + api_base_url: '' + diff --git a/configs/path_ignore.yaml b/configs/path_ignore.yaml new file mode 100644 index 000000000..149c6b2f1 --- /dev/null +++ b/configs/path_ignore.yaml @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# url path reserves the keywords list +questions: + - ask +tags: + - create +users: + - unsubscribe + - settings + - login + - register + - account-recovery + - change-email + - password-reset + - account-activation + - confirm-new-email + - account-suspended + - confirm-email + - auth-landing diff --git a/configs/reserved-usernames.json b/configs/reserved-usernames.json new file mode 100644 index 000000000..deb6c8229 --- /dev/null +++ b/configs/reserved-usernames.json @@ -0,0 +1 @@ +["0","100","101","102","1xx","200","201","202","203","204","205","206","207","226","2xx","300","301","302","303","304","305","307","308","3xx","400","401","402","403","404","405","406","407","408","409","410","411","412","413","414","415","416","417","418","422","423","424","426","428","429","431","451","4xx","500","501","502","503","504","505","506","507","511","5xx","7xx","about","abuse","access","account","account-activation","account-recovery","account-suspended","accounts","activate","activities","activity","ad","add","address","adm","admin","administration","administrator","ads","adult","advertising","affiliate","affiliates","ajax","all","alpha","analysis","analytics","android","anon","anonymous","api","app","apps","archive","archives","article","asct","asset","atom","auth","auth-landing","authentication","autoconfig","avatar","backup","balancer-manager","bank","banner","banners","beta","billing","bin","blog","blogs","board","book","bookmark","bot","bots","broadcasthost","bug","bugs","business","cache","cadastro","calendar","call","campaign","cancel","captcha","career","careers","cart","categories","category","cgi","cgi-bin","changelog","change-email","chat","check","checking","checkout","client","cliente","clients","code","codereview","comercial","comment","comments","communities","community","company","compare","compras","config","configuration","confirm-new-email","confirm-email","connect","contact","contact-us","contact_us","contactus","contest","contribute","corp","create","crypt","css","dashboard","data","db","default","delete","demo","design","designer","destroy","dev","devel","developer","developers","diagram","diary","dict","dictionary","die","dir","direct_messages","directory","dist","dns","doc","docker","docs","documentation","domain","download","downloads","ecommerce","edit","editor","edu","education","email","employment","empty","end","enterprise","entries","entry","error","errors","eval","event","everyone","exit","explore","export","facebook","faq","favorite","favorites","fbi","feature","features","feed","feedback","feeds","file","files","firewall","first","flash","fleet","fleets","flog","follow","followers","following","forgot","forgot-password","forgot_password","forgotpassword","form","forum","forums","founder","free","friend","friends","ftp","gadget","gadgets","game","games","get","ghost","gift","gifts","gist","git","github","graph","group","groups","guest","guests","help","home","homepage","hooks","host","hosting","hostmaster","hostname","howto","hpg","html","http","httpd","https","i","iamges","icon","icons","id","idea","ideas","image","images","imap","img","index","indice","info","information","inquiry","instagram","intranet","invitations","invite","ip","ipad","iphone","irc","is","isatap","issue","issues","it","item","items","java","javascript","job","jobs","join","js","json","jump","keys","keyserver","knowledgebase","language","languages","last","ldap-status","legal","license","link","links","linux","list","lists","local","localdomain","localhost","log","log-in","log-out","log_in","log_out","login","logout","logs","m","mac","mail","mail1","mail2","mail3","mail4","mail5","mailer","mailer-daemon","mailing","maintenance","manager","manual","map","maps","marketing","master","me","media","member","members","message","messages","messenger","microblog","microblogs","mine","mis","mob","mobile","movie","movies","mp3","msg","msn","music","musicas","mx","my","mysql","name","named","names","namespace","namespaces","nan","navi","navigation","net","network","new","news","newsletter","nick","nickname","no-reply","nobody","noc","noreply","notes","noticias","notification","notifications","notify","ns","ns1","ns10","ns2","ns3","ns4","ns5","ns6","ns7","ns8","ns9","null","oauth","oauth_clients","offer","offers","official","old","online","openid","operator","ops","order","orders","organization","organizations","orgs","overview","owner","owners","package","page","pager","pages","panel","passwd","password","password-reset","patch","payment","perl","phone","photo","photoalbum","photos","php","phpmyadmin","phppgadmin","phpredisadmin","pic","pics","ping","plan","plans","plugin","plugins","policy","pop","pop3","popular","portal","post","postfix","postmaster","posts","pr","premium","press","price","pricing","privacy","privacy-policy","privacy_policy","privacypolicy","private","product","products","profile","project","projects","promo","pub","public","purpose","put","pw","python","query","random","ranking","read","readme","recent","recruit","recruitment","register","registration","release","releases","remote","remove","replies","reply","report","reports","repositories","repository","req","request","requests","res","reset","reset-password","reset_password","resetpassword","resource","resources","roc","root","rss","ruby","rule","rules","sag","sale","sales","sample","samples","save","school","script","scripts","search","secure","security","self","send","server","server-info","server-status","service","services","session","sessions","setting","settings","setup","share","shop","show","sign-in","sign-up","sign_in","sign_up","signin","signout","signup","site","sitemap","sites","smartphone","smtp","soporte","source","spec","special","sql","src","ssh","ssl","ssladmin","ssladministrator","sslwebmaster","staff","stage","staging","start","stat","state","static","stats","status","store","stores","stories","style","styleguide","styles","stylesheet","stylesheets","subdomain","subscribe","subscriptions","suporte","support","svn","swf","sys","sysadmin","sysadministrator","system","tablet","tablets","tag","tags","talk","task","tasks","team","teams","tech","telnet","term","terms","terms-of-service","terms_of_service","termsofservice","test","test1","test2","test3","teste","testing","tests","theme","themes","thread","threads","tls","tmp","todo","token","tokenserver","tool","tools","top","topic","topics","tos","tour","translations","trends","tutorial","tux","tv","twitter","undef","unfollow","unsubscribe","update","upload","uploads","uptime","url","usage","usenet","user","username","users","usr","usuario","util","uucp","vendas","ver","version","video","videos","visitor","vpn","watch","weather","web","webhook","webhooks","webmail","webmaster","website","websites","welcome","widget","widgets","wiki","win","windows","word","work","works","workshop","wpad","ww","wws","www","www1","www2","www3","www4","www5","www6","www7","wwws","wwww","xfn","xml","xmpp","xpg","xxx","yaml","year","yml","you","yourdomain","yourname","yoursite","yourusername"] \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..cdda4e718 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +files: + - source: /i18n/en_US.yaml + translation: /i18n/%locale_with_underscore%.yaml diff --git a/docker-compose.yaml b/docker-compose.yaml index dfa1853a7..58e8ea036 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,29 +1,29 @@ -version: "3.9" +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +version: "3" services: answer: - image: answerdev/answer:latest + image: apache/answer ports: - '9080:80' restart: on-failure - depends_on: - db: - condition: service_healthy - links: - - db volumes: - - ./answer-data/data:/data - db: - image: mariadb:10.4.7 - ports: - - '13306:3306' - restart: on-failure - environment: - MYSQL_DATABASE: answer - MYSQL_ROOT_PASSWORD: root - healthcheck: - test: [ "CMD", "mysqladmin" ,"ping", "-uroot", "-proot"] - timeout: 20s - retries: 10 - command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--skip-character-set-client-handshake'] - volumes: - - ./answer-data/mysql:/var/lib/mysql + - answer-data:/data + +volumes: + answer-data: diff --git a/docs/docs.go b/docs/docs.go index 51596b919..263e6775e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,23 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -16,6 +34,22 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/": { + "get": { + "description": "if config file not exist try to redirect to install page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "if config file not exist try to redirect to install page", + "responses": {} + } + }, "/answer/admin/api/answer/page": { "get": { "security": [ @@ -23,7 +57,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], @@ -33,7 +67,7 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "CmsSearchList", + "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", @@ -50,12 +84,25 @@ const docTemplate = `{ { "enum": [ "available", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "answer id or question title", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "question id", + "name": "question_id", + "in": "query" } ], "responses": { @@ -75,7 +122,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "update answer status", "consumes": [ "application/json" ], @@ -85,15 +132,15 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "AdminSetAnswerStatus", + "summary": "update answer status", "parameters": [ { - "description": "AdminSetAnswerStatusRequest", + "description": "AdminUpdateAnswerStatusReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/entity.AdminSetAnswerStatusRequest" + "$ref": "#/definitions/schema.AdminUpdateAnswerStatusReq" } } ], @@ -107,21 +154,35 @@ const docTemplate = `{ } } }, - "/answer/admin/api/language/options": { - "get": { + "/answer/admin/api/badge/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get language options", + "description": "update badge status", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Lang" + "AdminBadge" + ], + "summary": "update badge status", + "parameters": [ + { + "description": "UpdateBadgeStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateBadgeStatusReq" + } + } ], - "summary": "Get language options", "responses": { "200": { "description": "OK", @@ -132,14 +193,14 @@ const docTemplate = `{ } } }, - "/answer/admin/api/question/page": { + "/answer/admin/api/badges": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Status:[available,closed,deleted]", + "description": "list all badges by page", "consumes": [ "application/json" ], @@ -147,13 +208,13 @@ const docTemplate = `{ "application/json" ], "tags": [ - "admin" + "AdminBadge" ], - "summary": "CmsSearchList", + "summary": "list all badges by page", "parameters": [ { "type": "integer", - "description": "page size", + "description": "page", "name": "page", "in": "query" }, @@ -165,73 +226,55 @@ const docTemplate = `{ }, { "enum": [ - "available", - "closed", - "deleted" + "", + "active", + "inactive" ], "type": "string", - "description": "user status", + "description": "badge status", "name": "status", "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, - "/answer/admin/api/question/status": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Status:[available,closed,deleted]", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "AdminSetQuestionStatus", - "parameters": [ + }, { - "description": "AdminSetQuestionStatusRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AdminSetQuestionStatusRequest" - } + "type": "string", + "description": "search param", + "name": "q", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListPagedResp" + } + } + } + } + ] } } } } }, - "/answer/admin/api/reasons": { + "/answer/admin/api/dashboard": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get reasons by object type and action", + "description": "DashboardInfo", "consumes": [ "application/json" ], @@ -239,37 +282,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "reason" - ], - "summary": "get reasons by object type and action", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "enum": [ - "status", - "close", - "flag", - "review" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "admin" ], + "summary": "DashboardInfo", "responses": { "200": { "description": "OK", @@ -280,17 +295,14 @@ const docTemplate = `{ } } }, - "/answer/admin/api/report/": { - "put": { + "/answer/admin/api/delete/permanently": { + "delete": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "handle flag", + "description": "delete permanently", "consumes": [ "application/json" ], @@ -300,15 +312,15 @@ const docTemplate = `{ "tags": [ "admin" ], - "summary": "handle flag", + "summary": "delete permanently", "parameters": [ { - "description": "flag", + "description": "DeletePermanentlyReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.ReportHandleReq" + "$ref": "#/definitions/schema.DeletePermanentlyReq" } } ], @@ -322,65 +334,21 @@ const docTemplate = `{ } } }, - "/answer/admin/api/reports/page": { + "/answer/admin/api/language/options": { "get": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "list report records", - "consumes": [ - "application/json" - ], + "description": "Get language options", "produces": [ "application/json" ], "tags": [ - "admin" - ], - "summary": "list report page", - "parameters": [ - { - "enum": [ - "pending", - "completed" - ], - "type": "string", - "description": "status", - "name": "status", - "in": "query", - "required": true - }, - { - "enum": [ - "all", - "question", - "answer", - "comment" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - } + "Lang" ], + "summary": "Get language options", "responses": { "200": { "description": "OK", @@ -391,21 +359,30 @@ const docTemplate = `{ } } }, - "/answer/admin/api/setting/smtp": { + "/answer/admin/api/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetSMTPConfig get smtp config", + "description": "get plugin config", "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" + ], + "summary": "get plugin config", + "parameters": [ + { + "type": "string", + "description": "plugin_slug_name", + "name": "plugin_slug_name", + "in": "query", + "required": true + } ], - "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", @@ -418,7 +395,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetSMTPConfigResp" + "$ref": "#/definitions/schema.GetPluginConfigResp" } } } @@ -433,22 +410,25 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "update smtp config", + "description": "update plugin config", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "update smtp config", + "summary": "update plugin config", "parameters": [ { - "description": "smtp config", + "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateSMTPConfigReq" + "$ref": "#/definitions/schema.UpdatePluginConfigReq" } } ], @@ -462,64 +442,32 @@ const docTemplate = `{ } } }, - "/answer/admin/api/siteinfo/general": { - "get": { + "/answer/admin/api/plugin/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", - "produces": [ + "description": "update plugin status", + "consumes": [ "application/json" ], - "tags": [ - "admin" - ], - "summary": "Get siteinfo general", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get siteinfo interface", "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "Get siteinfo interface", + "summary": "update plugin status", "parameters": [ { - "description": "general", + "description": "UpdatePluginStatusReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.SiteGeneralReq" + "$ref": "#/definitions/schema.UpdatePluginStatusReq" } } ], @@ -533,30 +481,36 @@ const docTemplate = `{ } } }, - "/answer/admin/api/siteinfo/interface": { + "/answer/admin/api/plugins": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get plugin list", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "Get siteinfo interface", + "summary": "get plugin list", "parameters": [ { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } + "type": "string", + "description": "status: active/inactive", + "name": "status", + "in": "query" + }, + { + "type": "boolean", + "description": "have config", + "name": "have_config", + "in": "query" } ], "responses": { @@ -571,7 +525,10 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteInterfaceResp" + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetPluginListResp" + } } } } @@ -579,30 +536,56 @@ const docTemplate = `{ } } } - }, - "put": { + } + }, + "/answer/admin/api/question/page": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "Status:[available,closed,deleted,pending]", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "AdminQuestionPage admin question page", "parameters": [ { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.SiteInterfaceReq" - } + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "available", + "closed", + "deleted", + "pending" + ], + "type": "string", + "description": "user status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "question id or title", + "name": "query", + "in": "query" } ], "responses": { @@ -615,21 +598,35 @@ const docTemplate = `{ } } }, - "/answer/admin/api/theme/options": { - "get": { + "/answer/admin/api/question/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get theme options", + "description": "update question status", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get theme options", + "summary": "update question status", + "parameters": [ + { + "description": "AdminUpdateQuestionStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AdminUpdateQuestionStatusReq" + } + } + ], "responses": { "200": { "description": "OK", @@ -640,14 +637,14 @@ const docTemplate = `{ } } }, - "/answer/admin/api/user/status": { - "put": { + "/answer/admin/api/reasons": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update user", + "description": "get reasons by object type and action", "consumes": [ "application/json" ], @@ -655,18 +652,35 @@ const docTemplate = `{ "application/json" ], "tags": [ - "admin" + "reason" ], - "summary": "update user", + "summary": "get reasons by object type and action", "parameters": [ { - "description": "user", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserStatusReq" - } + "enum": [ + "question", + "answer", + "comment", + "user" + ], + "type": "string", + "description": "object_type", + "name": "object_type", + "in": "query", + "required": true + }, + { + "enum": [ + "status", + "close", + "flag", + "review" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true } ], "responses": { @@ -679,59 +693,21 @@ const docTemplate = `{ } } }, - "/answer/admin/api/users/page": { + "/answer/admin/api/roles": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get user page", + "description": "get role list", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "get user page", - "parameters": [ - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" - }, - { - "type": "string", - "description": "email", - "name": "e_mail", - "in": "query" - }, - { - "enum": [ - "normal", - "suspended", - "deleted", - "inactive" - ], - "type": "string", - "description": "user status", - "name": "status", - "in": "query" - } - ], + "summary": "get role list", "responses": { "200": { "description": "OK", @@ -744,22 +720,10 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "records": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetUserPageResp" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRoleResp" + } } } } @@ -769,69 +733,64 @@ const docTemplate = `{ } } }, - "/answer/api/v1/answer": { - "put": { + "/answer/admin/api/setting/privileges": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Update Answer", - "consumes": [ - "application/json" - ], + "description": "GetPrivilegesConfig get privileges config", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "Update Answer", - "parameters": [ - { - "description": "AnswerUpdateReq", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AnswerUpdateReq" - } - } + "admin" ], + "summary": "GetPrivilegesConfig get privileges config", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetPrivilegesConfigResp" + } + } + } + ] } } } }, - "post": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Insert Answer", - "consumes": [ - "application/json" - ], + "description": "update privileges config", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "Insert Answer", + "summary": "update privileges config", "parameters": [ { - "description": "AnswerAddReq", + "description": "config", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerAddReq" + "$ref": "#/definitions/schema.UpdatePrivilegesConfigReq" } } ], @@ -839,75 +798,70 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } - }, - "delete": { + } + }, + "/answer/admin/api/setting/smtp": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "delete answer", - "consumes": [ - "application/json" - ], + "description": "GetSMTPConfig get smtp config", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "delete answer", - "parameters": [ - { - "description": "answer", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RemoveAnswerReq" - } - } + "admin" ], + "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetSMTPConfigResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/answer/acceptance": { - "post": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Adopted", - "consumes": [ - "application/json" - ], + "description": "update smtp config", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "Adopted", + "summary": "update smtp config", "parameters": [ { - "description": "AnswerAdoptedReq", + "description": "smtp config", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerAdoptedReq" + "$ref": "#/definitions/schema.UpdateSMTPConfigReq" } } ], @@ -915,71 +869,70 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/answer/info": { + "/answer/admin/api/siteinfo/branding": { "get": { - "description": "Get Answer", - "consumes": [ - "application/json" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "get site interface", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "Get Answer", - "parameters": [ - { - "type": "string", - "default": "1", - "description": "Answer TagID", - "name": "id", - "in": "query", - "required": true - } + "admin" ], + "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteBrandingResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/answer/list": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", - "consumes": [ - "application/json" - ], + "description": "update site info branding", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "AnswerList", + "summary": "update site info branding", "parameters": [ { - "description": "AnswerList", + "description": "branding info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerList" + "$ref": "#/definitions/schema.SiteBrandingReq" } } ], @@ -987,41 +940,27 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/collection/switch": { - "post": { + "/answer/admin/api/siteinfo/custom-css-html": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add collection", - "consumes": [ - "application/json" - ], + "description": "get site info custom html css config", "produces": [ "application/json" ], "tags": [ - "Collection" - ], - "summary": "add collection", - "parameters": [ - { - "description": "collection", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.CollectionSwitchReq" - } - } + "admin" ], + "summary": "get site info custom html css config", "responses": { "200": { "description": "OK", @@ -1034,7 +973,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.CollectionSwitchResp" + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" } } } @@ -1042,27 +981,57 @@ const docTemplate = `{ } } } - } - }, - "/answer/api/v1/comment": { - "get": { - "description": "get comment by id", + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "get comment by id", + "summary": "update site custom css html config", "parameters": [ { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/general": { + "get": { + "security": [ + { + "ApiKeyAuth": [] } ], + "description": "get site general information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -1075,22 +1044,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentResp" - } - } - } - } - ] + "$ref": "#/definitions/schema.SiteGeneralResp" } } } @@ -1105,25 +1059,22 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "update comment", - "consumes": [ - "application/json" - ], + "description": "update site general information", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "update comment", + "summary": "update site general information", "parameters": [ { - "description": "comment", + "description": "general", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateCommentReq" + "$ref": "#/definitions/schema.SiteGeneralReq" } } ], @@ -1135,35 +1086,23 @@ const docTemplate = `{ } } } - }, - "post": { + } + }, + "/answer/admin/api/siteinfo/interface": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add comment", - "consumes": [ - "application/json" - ], + "description": "get site interface", "produces": [ "application/json" ], "tags": [ - "Comment" - ], - "summary": "add comment", - "parameters": [ - { - "description": "comment", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } + "admin" ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -1176,7 +1115,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetCommentResp" + "$ref": "#/definitions/schema.SiteInterfaceResp" } } } @@ -1185,31 +1124,28 @@ const docTemplate = `{ } } }, - "delete": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "remove comment", - "consumes": [ - "application/json" - ], + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "remove comment", + "summary": "update site info interface", "parameters": [ { - "description": "comment", + "description": "general", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.RemoveCommentReq" + "$ref": "#/definitions/schema.SiteInterfaceReq" } } ], @@ -1223,46 +1159,21 @@ const docTemplate = `{ } } }, - "/answer/api/v1/comment/page": { + "/answer/admin/api/siteinfo/legal": { "get": { - "description": "get comment page", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Set the legal information for the site", "produces": [ "application/json" ], "tags": [ - "Comment" - ], - "summary": "get comment page", - "parameters": [ - { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "object id", - "name": "object_id", - "in": "query", - "required": true - }, - { - "enum": [ - "vote" - ], - "type": "string", - "description": "query condition", - "name": "query_cond", - "in": "query" - } + "admin" ], + "summary": "Set the legal information for the site", "responses": { "200": { "description": "OK", @@ -1275,22 +1186,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentResp" - } - } - } - } - ] + "$ref": "#/definitions/schema.SiteLegalResp" } } } @@ -1298,37 +1194,57 @@ const docTemplate = `{ } } } - } - }, - "/answer/api/v1/follow": { - "post": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "follow object or cancel follow operation", - "consumes": [ - "application/json" - ], + "description": "update site legal info", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" ], - "summary": "follow object or cancel follow operation", + "summary": "update site legal info", "parameters": [ { - "description": "follow", + "description": "write info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.FollowReq" + "$ref": "#/definitions/schema.SiteLegalReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" } } + } + } + }, + "/answer/admin/api/siteinfo/login": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info login config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" ], + "summary": "get site info login config", "responses": { "200": { "description": "OK", @@ -1341,7 +1257,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.FollowResp" + "$ref": "#/definitions/schema.SiteLoginResp" } } } @@ -1349,34 +1265,29 @@ const docTemplate = `{ } } } - } - }, - "/answer/api/v1/follow/tags": { + }, "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update user follow tags", - "consumes": [ - "application/json" - ], + "description": "update site login", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" ], - "summary": "update user follow tags", + "summary": "update site login", "parameters": [ { - "description": "follow", + "description": "login info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateFollowTagsReq" + "$ref": "#/definitions/schema.SiteLoginReq" } } ], @@ -1390,23 +1301,65 @@ const docTemplate = `{ } } }, - "/answer/api/v1/language/config": { + "/answer/admin/api/siteinfo/seo": { "get": { - "description": "get language config mapping", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site seo information", "produces": [ "application/json" ], "tags": [ - "Lang" + "admin" ], - "summary": "get language config mapping", + "summary": "get site seo information", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteSeoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site seo information", "parameters": [ { - "type": "string", - "description": "Accept-Language", - "name": "Accept-Language", - "in": "header", - "required": true + "description": "seo", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteSeoReq" + } } ], "responses": { @@ -1419,72 +1372,65 @@ const docTemplate = `{ } } }, - "/answer/api/v1/language/options": { + "/answer/admin/api/siteinfo/theme": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get language options", + "description": "get site info theme config", "produces": [ "application/json" ], "tags": [ - "Lang" + "admin" ], - "summary": "Get language options", + "summary": "get site info theme config", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/notification/page": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get notification list", - "consumes": [ - "application/json" - ], + "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "get notification list", + "summary": "update site custom css html config", "parameters": [ { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "enum": [ - "inbox", - "achievement" - ], - "type": "string", - "description": "type", - "name": "type", - "in": "query", - "required": true + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteThemeReq" + } } ], "responses": { @@ -1497,32 +1443,64 @@ const docTemplate = `{ } } }, - "/answer/api/v1/notification/read/state": { - "put": { + "/answer/admin/api/siteinfo/users": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "ClearUnRead", - "consumes": [ + "description": "get site user config", + "produces": [ "application/json" ], + "tags": [ + "admin" + ], + "summary": "get site user config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteUsersResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site info config about users", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "ClearUnRead", + "summary": "update site info config about users", "parameters": [ { - "description": "NotificationClearIDRequest", + "description": "users info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearIDRequest" + "$ref": "#/definitions/schema.SiteUsersReq" } } ], @@ -1536,32 +1514,64 @@ const docTemplate = `{ } } }, - "/answer/api/v1/notification/read/state/all": { - "put": { + "/answer/admin/api/siteinfo/write": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "ClearUnRead", - "consumes": [ + "description": "get site interface", + "produces": [ "application/json" ], + "tags": [ + "admin" + ], + "summary": "get site interface", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteWriteResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site write info", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "ClearUnRead", + "summary": "update site write info", "parameters": [ { - "description": "NotificationClearRequest", + "description": "write info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearRequest" + "$ref": "#/definitions/schema.SiteWriteReq" } } ], @@ -1575,24 +1585,21 @@ const docTemplate = `{ } } }, - "/answer/api/v1/notification/status": { + "/answer/admin/api/theme/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetRedDot", - "consumes": [ - "application/json" - ], + "description": "Get theme options", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "GetRedDot", + "summary": "Get theme options", "responses": { "200": { "description": "OK", @@ -1601,14 +1608,16 @@ const docTemplate = `{ } } } - }, - "put": { + } + }, + "/answer/admin/api/user": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "DelRedDot", + "description": "add user", "consumes": [ "application/json" ], @@ -1616,17 +1625,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "DelRedDot", + "summary": "add user", "parameters": [ { - "description": "NotificationClearRequest", + "description": "user", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearRequest" + "$ref": "#/definitions/schema.AddUserReq" } } ], @@ -1640,57 +1649,26 @@ const docTemplate = `{ } } }, - "/answer/api/v1/personal/answer/page": { + "/answer/admin/api/user/activation": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserAnswerList", - "consumes": [ - "application/json" - ], + "description": "get user activation", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "UserAnswerList", + "summary": "get user activation", "parameters": [ { "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "score" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "0", - "description": "page", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", + "description": "user id", + "name": "user_id", "in": "query", "required": true } @@ -1699,20 +1677,32 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserActivationResp" + } + } + } + ] } } } } }, - "/answer/api/v1/personal/collection/page": { - "get": { + "/answer/admin/api/user/password": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserCollectionList", + "description": "update user password", "consumes": [ "application/json" ], @@ -1720,25 +1710,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Collection" + "admin" ], - "summary": "UserCollectionList", + "summary": "update user password", "parameters": [ { - "type": "string", - "default": "0", - "description": "page", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPasswordReq" + } } ], "responses": { @@ -1751,81 +1734,53 @@ const docTemplate = `{ } } }, - "/answer/api/v1/personal/comment/page": { - "get": { - "description": "user personal comment list", + "/answer/admin/api/user/profile": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "edit user profile", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "user personal comment list", + "summary": "edit user profile", "parameters": [ { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.EditUserProfileReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/qa/top": { - "get": { + "/answer/admin/api/user/role": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserTop", + "description": "update user role", "consumes": [ "application/json" ], @@ -1833,17 +1788,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "admin" ], - "summary": "UserTop", + "summary": "update user role", "parameters": [ { - "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserRoleReq" + } } ], "responses": { @@ -1856,81 +1812,53 @@ const docTemplate = `{ } } }, - "/answer/api/v1/personal/rank/page": { - "get": { - "description": "user personal rank list", + "/answer/admin/api/user/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Rank" + "admin" ], - "summary": "user personal rank list", + "summary": "update user", "parameters": [ { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserStatusReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRankPersonalWithPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/user/info": { - "get": { + "/answer/admin/api/users": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetOtherUserInfoByUsername", + "description": "add users", "consumes": [ "application/json" ], @@ -1938,58 +1866,81 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "admin" ], - "summary": "GetOtherUserInfoByUsername", + "summary": "add users", "parameters": [ { - "type": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddUsersReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetOtherUserInfoResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/vote/page": { - "get": { + "/answer/admin/api/users/activation": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "user's vote", - "consumes": [ - "application/json" - ], + "description": "send user activation", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" + ], + "summary": "send user activation", + "parameters": [ + { + "description": "SendUserActivationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SendUserActivationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/users/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user page", + "produces": [ + "application/json" ], - "summary": "user's votes", + "tags": [ + "admin" + ], + "summary": "get user page", "parameters": [ { "type": "integer", @@ -2002,6 +1953,29 @@ const docTemplate = `{ "description": "page size", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "search query: email, username or id:[id]", + "name": "query", + "in": "query" + }, + { + "type": "boolean", + "description": "staff user", + "name": "staff", + "in": "query" + }, + { + "enum": [ + "suspended", + "deleted", + "inactive" + ], + "type": "string", + "description": "user status", + "name": "status", + "in": "query" } ], "responses": { @@ -2023,10 +1997,10 @@ const docTemplate = `{ { "type": "object", "properties": { - "list": { + "records": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetVoteWithPageResp" + "$ref": "#/definitions/schema.GetUserPageResp" } } } @@ -2041,14 +2015,118 @@ const docTemplate = `{ } } }, - "/answer/api/v1/question": { + "/answer/api/v1/activity/timeline": { + "get": { + "description": "get object timeline", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query" + }, + { + "type": "string", + "description": "tag slug name", + "name": "tag_slug_name", + "in": "query" + }, + { + "enum": [ + "question", + "answer", + "tag" + ], + "type": "string", + "description": "object type", + "name": "object_type", + "in": "query" + }, + { + "type": "boolean", + "description": "is show vote", + "name": "show_vote", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/activity/timeline/detail": { + "get": { + "description": "get object timeline detail", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline detail", + "parameters": [ + { + "type": "string", + "description": "revision id", + "name": "revision_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/answer": { "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update question", + "description": "Update Answer", "consumes": [ "application/json" ], @@ -2056,17 +2134,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "update question", + "summary": "Update Answer", "parameters": [ { - "description": "question", + "description": "AnswerUpdateReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionUpdate" + "$ref": "#/definitions/schema.AnswerUpdateReq" } } ], @@ -2085,7 +2163,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "add question", + "description": "add answer", "consumes": [ "application/json" ], @@ -2093,17 +2171,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "add question", + "summary": "Add Answer", "parameters": [ { - "description": "question", + "description": "add answer request", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionAdd" + "$ref": "#/definitions/schema.AnswerAddReq" } } ], @@ -2122,7 +2200,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "delete question", + "description": "delete answer", "consumes": [ "application/json" ], @@ -2130,17 +2208,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "delete question", + "summary": "delete answer", "parameters": [ { - "description": "question", + "description": "answer", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.RemoveQuestionReq" + "$ref": "#/definitions/schema.RemoveAnswerReq" } } ], @@ -2154,14 +2232,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/question/closemsglist": { - "get": { + "/answer/api/v1/answer/acceptance": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "close question msg list", + "description": "Accept Answer", "consumes": [ "application/json" ], @@ -2169,9 +2247,20 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" + ], + "summary": "Accept Answer", + "parameters": [ + { + "description": "AcceptAnswerReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AcceptAnswerReq" + } + } ], - "summary": "close question msg list", "responses": { "200": { "description": "OK", @@ -2182,14 +2271,9 @@ const docTemplate = `{ } } }, - "/answer/api/v1/question/info": { + "/answer/api/v1/answer/info": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "GetQuestion Question", + "description": "Get Answer Detail", "consumes": [ "application/json" ], @@ -2197,14 +2281,13 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "GetQuestion Question", + "summary": "Get Answer Detail", "parameters": [ { "type": "string", - "default": "1", - "description": "Question TagID", + "description": "id", "name": "id", "in": "query", "required": true @@ -2214,15 +2297,27 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetAnswerInfoResp" + } + } + } + ] } } } } }, - "/answer/api/v1/question/page": { + "/answer/api/v1/answer/page": { "get": { - "description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)", + "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", "consumes": [ "application/json" ], @@ -2230,18 +2325,37 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "SearchQuestionList", + "summary": "AnswerList", "parameters": [ { - "description": "QuestionSearch", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.QuestionSearch" - } + "type": "string", + "description": "question_id", + "name": "question_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true } ], "responses": { @@ -2254,9 +2368,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/question/search": { + "/answer/api/v1/answer/recover": { "post": { - "description": "SearchQuestionList", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover the deleted answer", "consumes": [ "application/json" ], @@ -2264,17 +2383,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "SearchQuestionList", + "summary": "recover answer", "parameters": [ { - "description": "QuestionSearch", + "description": "answer", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionSearch" + "$ref": "#/definitions/schema.RecoverAnswerReq" } } ], @@ -2282,20 +2401,15 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/question/similar": { + "/answer/api/v1/badge": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "add question title like", + "description": "get badge info", "consumes": [ "application/json" ], @@ -2303,15 +2417,15 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "add question title like", + "summary": "get badge info", "parameters": [ { "type": "string", "default": "string", - "description": "title", - "name": "title", + "description": "id", + "name": "id", "in": "query", "required": true } @@ -2320,15 +2434,27 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" - } + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } } } } }, - "/answer/api/v1/question/similar/tag": { + "/answer/api/v1/badge/awards/page": { "get": { - "description": "Search Similar Question", + "description": "get badge award list", "consumes": [ "application/json" ], @@ -2336,37 +2462,61 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "Search Similar Question", + "summary": "get badge award list", "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, { "type": "string", - "default": "", - "description": "question_id", - "name": "question_id", + "description": "badge id", + "name": "badge_id", "in": "query", "required": true + }, + { + "type": "string", + "description": "only list the award by username", + "name": "username", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] } } } } }, - "/answer/api/v1/question/status": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Close question", + "/answer/api/v1/badge/user/awards": { + "get": { + "description": "get user badge award list", "consumes": [ "application/json" ], @@ -2374,51 +2524,63 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "Close question", + "summary": "get user badge award list", "parameters": [ { - "description": "question", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.CloseQuestionReq" - } + "type": "string", + "description": "user name", + "name": "username", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] } } } } }, - "/answer/api/v1/question/tags": { + "/answer/api/v1/badge/user/awards/recent": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } + "description": "get user badge award list", + "consumes": [ + "application/json" ], - "description": "get tag list", "produces": [ "application/json" ], "tags": [ - "Tag" + "api-badge" ], - "summary": "get tag list", + "summary": "get user badge award list", "parameters": [ { "type": "string", - "description": "tag", - "name": "tag", - "in": "query" + "description": "user name", + "name": "username", + "in": "query", + "required": true } ], "responses": { @@ -2435,7 +2597,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } @@ -2446,14 +2608,9 @@ const docTemplate = `{ } } }, - "/answer/api/v1/reasons": { + "/answer/api/v1/badges": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "get reasons by object type and action", + "description": "list all badges group by group", "consumes": [ "application/json" ], @@ -2461,58 +2618,42 @@ const docTemplate = `{ "application/json" ], "tags": [ - "reason" - ], - "summary": "get reasons by object type and action", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "enum": [ - "status", - "close", - "flag", - "review" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "api-badge" ], + "summary": "list all badges group by group", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListResp" + } + } + } + } + ] } } } } }, - "/answer/api/v1/report": { + "/answer/api/v1/collection/switch": { "post": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "add report \u003cbr\u003e source (question, answer, comment, user)", + "description": "add collection", "consumes": [ "application/json" ], @@ -2520,54 +2661,19 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Report" + "Collection" ], - "summary": "add report", + "summary": "add collection", "parameters": [ { - "description": "report", + "description": "collection", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AddReportReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" + "$ref": "#/definitions/schema.CollectionSwitchReq" } } - } - } - }, - "/answer/api/v1/report/type/list": { - "get": { - "description": "get report type list", - "produces": [ - "application/json" - ], - "tags": [ - "Report" - ], - "summary": "get report type list", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "report source", - "name": "source", - "in": "query", - "required": true - } ], "responses": { "200": { @@ -2581,10 +2687,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetReportTypeResp" - } + "$ref": "#/definitions/schema.CollectionSwitchResp" } } } @@ -2594,21 +2697,21 @@ const docTemplate = `{ } } }, - "/answer/api/v1/revisions": { + "/answer/api/v1/comment": { "get": { - "description": "get revision list", + "description": "get comment by id", "produces": [ "application/json" ], "tags": [ - "Revision" + "Comment" ], - "summary": "get revision list", + "summary": "get comment by id", "parameters": [ { "type": "string", - "description": "object id", - "name": "object_id", + "description": "id", + "name": "id", "in": "query", "required": true } @@ -2625,10 +2728,22 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRevisionResp" - } + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentResp" + } + } + } + } + ] } } } @@ -2636,102 +2751,51 @@ const docTemplate = `{ } } } - } - }, - "/answer/api/v1/search": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "search object", + "description": "update comment", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Search" + "Comment" ], - "summary": "search object", + "summary": "update comment", "parameters": [ { - "type": "string", - "description": "query string", - "name": "q", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "active", - "score", - "relevance" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", + "description": "comment", + "name": "data", + "in": "body", + "required": true, "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SearchListResp" - } - } - } - ] + "$ref": "#/definitions/schema.UpdateCommentReq" } } - } - } - }, - "/answer/api/v1/siteinfo": { - "get": { - "description": "Get siteinfo", - "produces": [ - "application/json" - ], - "tags": [ - "site" ], - "summary": "Get siteinfo", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } - } - }, - "/answer/api/v1/tag": { - "get": { - "description": "get tag one", + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add comment", "consumes": [ "application/json" ], @@ -2739,23 +2803,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "get tag one", + "summary": "add comment", "parameters": [ { - "type": "string", - "description": "tag id", - "name": "tag_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "tag name", - "name": "tag_name", - "in": "query", - "required": true + "description": "comment", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddCommentReq" + } } ], "responses": { @@ -2770,7 +2829,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetCommentResp" } } } @@ -2779,8 +2838,13 @@ const docTemplate = `{ } } }, - "put": { - "description": "update tag", + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "remove comment", "consumes": [ "application/json" ], @@ -2788,17 +2852,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "update tag", + "summary": "remove comment", "parameters": [ { - "description": "tag", + "description": "comment", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateTagReq" + "$ref": "#/definitions/schema.RemoveCommentReq" } } ], @@ -2810,43 +2874,88 @@ const docTemplate = `{ } } } - }, - "delete": { - "description": "delete tag", - "consumes": [ - "application/json" - ], + } + }, + "/answer/api/v1/comment/page": { + "get": { + "description": "get comment page", "produces": [ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "delete tag", + "summary": "get comment page", "parameters": [ { - "description": "tag", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RemoveTagReq" - } + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + }, + { + "enum": [ + "vote" + ], + "type": "string", + "description": "query condition", + "name": "query_cond", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentResp" + } + } + } + } + ] + } + } + } + ] } } } } }, - "/answer/api/v1/tag/synonym": { - "put": { - "description": "update tag", + "/answer/api/v1/connector/binding/email": { + "post": { + "description": "external login binding user send email", "consumes": [ "application/json" ], @@ -2854,17 +2963,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "update tag", + "summary": "external login binding user send email", "parameters": [ { - "description": "tag", + "description": "external login binding user send email", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateTagSynonymReq" + "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq" } } ], @@ -2872,31 +2981,39 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp" + } + } + } + ] } } } } }, - "/answer/api/v1/tag/synonyms": { + "/answer/api/v1/connector/info": { "get": { - "description": "get tag synonyms", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get all enabled connectors", "produces": [ "application/json" ], "tags": [ - "Tag" - ], - "summary": "get tag synonyms", - "parameters": [ - { - "type": "integer", - "description": "tag id", - "name": "tag_id", - "in": "query", - "required": true - } + "PluginConnector" ], + "summary": "get all enabled connectors", "responses": { "200": { "description": "OK", @@ -2911,7 +3028,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagSynonymsResp" + "$ref": "#/definitions/schema.ConnectorInfoResp" } } } @@ -2922,21 +3039,21 @@ const docTemplate = `{ } } }, - "/answer/api/v1/tags/following": { + "/answer/api/v1/connector/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get following tag list", + "description": "get all connectors info about user", "produces": [ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "get following tag list", + "summary": "get all connectors info about user", "responses": { "200": { "description": "OK", @@ -2951,7 +3068,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetFollowingTagsResp" + "$ref": "#/definitions/schema.ConnectorUserInfoResp" } } } @@ -2962,110 +3079,58 @@ const docTemplate = `{ } } }, - "/answer/api/v1/tags/page": { - "get": { - "description": "get tag page", + "/answer/api/v1/connector/user/unbinding": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "unbind external user login", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "get tag page", + "summary": "unbind external user login", "parameters": [ { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "slug_name", - "name": "slug_name", - "in": "query" - }, - { - "enum": [ - "popular", - "name", - "newest" - ], - "type": "string", - "description": "query condition", - "name": "query_cond", - "in": "query" + "description": "ExternalLoginUnbindingReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ExternalLoginUnbindingReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetTagPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/action/record": { + "/answer/api/v1/embed/config": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } + "description": "get embed plugin config", + "consumes": [ + "application/json" ], - "description": "ActionRecord", - "tags": [ - "User" + "produces": [ + "application/json" ], - "summary": "ActionRecord", - "parameters": [ - { - "enum": [ - "login", - "e_mail", - "find_pass" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "tags": [ + "Plugin" ], + "summary": "get embed plugin config", "responses": { "200": { "description": "OK", @@ -3078,7 +3143,10 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.ActionRecordResp" + "type": "array", + "items": { + "$ref": "#/definitions/plugin.EmbedConfig" + } } } } @@ -3088,22 +3156,35 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/avatar/upload": { + "/answer/api/v1/file": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserUpdateInfo", + "description": "upload file", "consumes": [ "multipart/form-data" ], "tags": [ - "User" + "Upload" ], - "summary": "UserUpdateInfo", + "summary": "upload file", "parameters": [ + { + "enum": [ + "post", + "post_attachment", + "avatar", + "branding" + ], + "type": "string", + "description": "identify the source of the file upload", + "name": "source", + "in": "formData", + "required": true + }, { "type": "file", "description": "file", @@ -3134,14 +3215,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/email": { - "put": { + "/answer/api/v1/follow": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "user change email verification", + "description": "follow object or cancel follow operation", "consumes": [ "application/json" ], @@ -3149,17 +3230,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Activity" ], - "summary": "user change email verification", + "summary": "follow object or cancel follow operation", "parameters": [ { - "description": "UserChangeEmailVerifyReq", + "description": "follow", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" + "$ref": "#/definitions/schema.FollowReq" } } ], @@ -3167,15 +3248,32 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.FollowResp" + } + } + } + ] } } } } }, - "/answer/api/v1/user/email/change/code": { - "post": { - "description": "send email to the user email then change their email", + "/answer/api/v1/follow/tags": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user follow tags", "consumes": [ "application/json" ], @@ -3183,17 +3281,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Activity" ], - "summary": "send email to the user email then change their email", + "summary": "update user follow tags", "parameters": [ { - "description": "UserChangeEmailSendCodeReq", + "description": "follow", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" + "$ref": "#/definitions/schema.UpdateFollowTagsReq" } } ], @@ -3207,26 +3305,22 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/email/verification": { - "post": { - "description": "UserVerifyEmail", - "consumes": [ - "application/json" - ], + "/answer/api/v1/language/config": { + "get": { + "description": "get language config mapping", "produces": [ "application/json" ], "tags": [ - "User" + "Lang" ], - "summary": "UserVerifyEmail", + "summary": "get language config mapping", "parameters": [ { "type": "string", - "default": "", - "description": "code", - "name": "code", - "in": "query", + "description": "Accept-Language", + "name": "Accept-Language", + "in": "header", "required": true } ], @@ -3234,76 +3328,35 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/email/verification/send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserVerifyEmailSend", - "consumes": [ - "application/json" - ], + "/answer/api/v1/language/options": { + "get": { + "description": "Get language options", "produces": [ "application/json" ], "tags": [ - "User" - ], - "summary": "UserVerifyEmailSend", - "parameters": [ - { - "type": "string", - "default": "", - "description": "captcha_id", - "name": "captcha_id", - "in": "query" - }, - { - "type": "string", - "default": "", - "description": "captcha_code", - "name": "captcha_code", - "in": "query" - } + "Lang" ], + "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/info": { + "/answer/api/v1/meta/reaction": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "GetUserInfoByUserID", + "description": "get reaction for an object", "consumes": [ "application/json" ], @@ -3311,9 +3364,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Meta" + ], + "summary": "get reaction", + "parameters": [ + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query", + "required": true + } ], - "summary": "GetUserInfoByUserID", "responses": { "200": { "description": "OK", @@ -3326,7 +3388,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetUserResp" + "$ref": "#/definitions/schema.ReactionRespItem" } } } @@ -3341,7 +3403,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "UserUpdateInfo update user info", + "description": "update reaction. if not exist, add one", "consumes": [ "application/json" ], @@ -3349,24 +3411,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Meta" ], - "summary": "UserUpdateInfo update user info", + "summary": "add or update reaction", "parameters": [ { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", + "description": "reaction", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateInfoRequest" + "$ref": "#/definitions/schema.UpdateReactionReq" } } ], @@ -3380,9 +3435,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/login/email": { - "post": { - "description": "UserEmailLogin", + "/answer/api/v1/notification/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get notification list", "consumes": [ "application/json" ], @@ -3390,55 +3450,47 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserEmailLogin", + "summary": "get notification list", "parameters": [ { - "description": "UserEmailLogin", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserEmailLogin" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] - } + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "inbox", + "achievement" + ], + "type": "string", + "description": "type", + "name": "type", + "in": "query", + "required": true + }, + { + "enum": [ + "all", + "posts", + "invites", + "votes" + ], + "type": "string", + "description": "inbox_type", + "name": "inbox_type", + "in": "query", + "required": true } - } - } - }, - "/answer/api/v1/user/logout": { - "get": { - "description": "user logout", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" ], - "summary": "user logout", "responses": { "200": { "description": "OK", @@ -3449,14 +3501,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/notice/set": { - "post": { + "/answer/api/v1/notification/read/state": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserNoticeSet", + "description": "ClearUnRead", "consumes": [ "application/json" ], @@ -3464,17 +3516,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserNoticeSet", + "summary": "ClearUnRead", "parameters": [ { - "description": "UserNoticeSetRequest", + "description": "NotificationClearIDRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserNoticeSetRequest" + "$ref": "#/definitions/schema.NotificationClearIDRequest" } } ], @@ -3482,32 +3534,20 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.UserNoticeSetResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/password": { + "/answer/api/v1/notification/read/state/all": { "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserModifyPassWord", + "description": "ClearUnRead", "consumes": [ "application/json" ], @@ -3515,17 +3555,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserModifyPassWord", + "summary": "ClearUnRead", "parameters": [ { - "description": "UserModifyPassWordRequest", + "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserModifyPassWordRequest" + "$ref": "#/definitions/schema.NotificationClearRequest" } } ], @@ -3539,9 +3579,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/password/replacement": { - "post": { - "description": "UseRePassWord", + "/answer/api/v1/notification/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "GetRedDot", "consumes": [ "application/json" ], @@ -3549,33 +3594,25 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" - ], - "summary": "UseRePassWord", - "parameters": [ - { - "description": "UserRePassWordRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserRePassWordRequest" - } - } + "Notification" ], + "summary": "GetRedDot", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } - } - }, - "/answer/api/v1/user/password/reset": { - "post": { - "description": "RetrievePassWord", + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DelRedDot", "consumes": [ "application/json" ], @@ -3583,17 +3620,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "RetrievePassWord", + "summary": "DelRedDot", "parameters": [ { - "description": "UserRetrievePassWordRequest", + "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserRetrievePassWordRequest" + "$ref": "#/definitions/schema.NotificationClearRequest" } } ], @@ -3601,33 +3638,80 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/post/file": { - "post": { + "/answer/api/v1/permission": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "upload user post file", - "consumes": [ - "multipart/form-data" + "description": "check user permission", + "produces": [ + "application/json" ], "tags": [ - "User" + "Permission" ], - "summary": "upload user post file", + "summary": "check user permission", "parameters": [ { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "enum": [ + "question.add", + "question.edit", + "question.edit_without_review", + "question.delete", + "question.close", + "question.reopen", + "question.vote_up", + "question.vote_down", + "question.pin", + "question.unpin", + "question.hide", + "question.show", + "answer.add", + "answer.edit", + "answer.edit_without_review", + "answer.delete", + "answer.accept", + "answer.vote_up", + "answer.vote_down", + "answer.invite_someone_to_answer", + "comment.add", + "comment.edit", + "comment.delete", + "comment.vote_up", + "comment.vote_down", + "report.add", + "tag.add", + "tag.edit", + "tag.edit_slug_name", + "tag.edit_without_review", + "tag.delete", + "tag.synonym", + "link.url_limit", + "vote.detail", + "answer.audit", + "question.audit", + "tag.audit", + "tag.use_reserved_tag" + ], + "type": "string", + "description": "permission key", + "name": "action", + "in": "query", "required": true } ], @@ -3643,7 +3727,10 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "type": "string" + "type": "object", + "additionalProperties": { + "type": "boolean" + } } } } @@ -3653,9 +3740,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/register/email": { - "post": { - "description": "UserRegisterByEmail", + "/answer/api/v1/personal/answer/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list personal answers", "consumes": [ "application/json" ], @@ -3663,50 +3755,64 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Personal" ], - "summary": "UserRegisterByEmail", + "summary": "list personal answers", "parameters": [ { - "description": "UserRegisterReq", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserRegisterReq" - } + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "score" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/status": { + "/answer/api/v1/personal/collection/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get user status info", + "description": "list personal collections", "consumes": [ "application/json" ], @@ -3714,39 +3820,107 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Collection" ], - "summary": "get user status info", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "summary": "list personal collections", + "parameters": [ + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/vote/down": { - "post": { - "security": [ + "/answer/api/v1/personal/comment/page": { + "get": { + "description": "user personal comment list", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "user personal comment list", + "parameters": [ { - "ApiKeyAuth": [] + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "username", + "name": "username", + "in": "query" } ], - "description": "add vote", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/personal/qa/top": { + "get": { + "description": "UserTop", "consumes": [ "application/json" ], @@ -3754,19 +3928,58 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Activity" + "Question" ], - "summary": "vote down", + "summary": "UserTop", "parameters": [ { - "description": "vote", - "name": "data", - "in": "body", - "required": true, + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/schema.VoteReq" + "$ref": "#/definitions/handler.RespBody" } } + } + } + }, + "/answer/api/v1/personal/rank/page": { + "get": { + "description": "user personal rank list", + "produces": [ + "application/json" + ], + "tags": [ + "Rank" + ], + "summary": "user personal rank list", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "username", + "name": "username", + "in": "query" + } ], "responses": { "200": { @@ -3780,7 +3993,22 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.VoteResp" + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRankPersonalPageResp" + } + } + } + } + ] } } } @@ -3790,14 +4018,14 @@ const docTemplate = `{ } } }, - "/answer/api/v1/vote/up": { - "post": { + "/answer/api/v1/personal/user/info": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add vote", + "description": "GetOtherUserInfoByUsername", "consumes": [ "application/json" ], @@ -3805,18 +4033,16 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Activity" + "User" ], - "summary": "vote up", + "summary": "GetOtherUserInfoByUsername", "parameters": [ { - "description": "vote", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.VoteReq" - } + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true } ], "responses": { @@ -3831,7 +4057,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.VoteResp" + "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } @@ -3841,14 +4067,14 @@ const docTemplate = `{ } } }, - "/personal/question/page": { + "/answer/api/v1/personal/vote/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserList", + "description": "get user personal votes", "consumes": [ "application/json" ], @@ -3856,1448 +4082,7238 @@ const docTemplate = `{ "application/json" ], "tags": [ - "api-question" + "Activity" ], - "summary": "UserList", + "summary": "get user personal votes", "parameters": [ { - "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "score" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "0", - "description": "page", + "type": "integer", + "description": "page size", "name": "page", - "in": "query", - "required": true + "in": "query" }, { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", - "in": "query", - "required": true + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetVoteWithPageResp" + } + } + } + } + ] + } + } + } + ] } } } } - } + }, + "/answer/api/v1/plugin/status": { + "get": { + "description": "get all plugins status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "get all plugins status", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetPluginListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/post/render": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "render post content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Upload" + ], + "summary": "render post content", + "parameters": [ + { + "description": "PostRenderReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.PostRenderReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAdd" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "delete question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RemoveQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/answer": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question and answer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question and answer", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAddByAnswer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/info": { + "get": { + "description": "get question details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question details", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question TagID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/question/invite": { + "get": { + "description": "get question invite user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question invite user info", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question invite user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question invite user", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdateInviteUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/link": { + "get": { + "description": "get question link", + "tags": [ + "Question" + ], + "summary": "get question link", + "parameters": [ + { + "minimum": 1, + "type": "integer", + "name": "in_days", + "in": "query" + }, + { + "enum": [ + "newest", + "active", + "hot", + "score", + "unanswered", + "recommend", + "frequent" + ], + "type": "string", + "name": "order", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "name": "question_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/operation": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Operation question \\n operation [pin unpin hide show]", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Operation question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.OperationQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/page": { + "get": { + "description": "get questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover deleted question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "recover deleted question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionRecoverReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/reopen": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "reopen question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "reopen question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReopenQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/similar": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "fuzzy query similar questions based on title", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "fuzzy query similar questions based on title", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "title", + "name": "title", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/similar/tag": { + "get": { + "description": "Search Similar Question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Search Similar Question", + "parameters": [ + { + "type": "string", + "default": "", + "description": "question_id", + "name": "question_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/question/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Close question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Close question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.CloseQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/tags": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get tag list", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag list", + "parameters": [ + { + "type": "string", + "description": "tag", + "name": "tag", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/reasons": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get reasons by object type and action", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "reason" + ], + "summary": "get reasons by object type and action", + "parameters": [ + { + "enum": [ + "question", + "answer", + "comment", + "user" + ], + "type": "string", + "description": "object_type", + "name": "object_type", + "in": "query", + "required": true + }, + { + "enum": [ + "status", + "close", + "flag", + "review" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/render/config": { + "get": { + "description": "GetRenderConfig", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PluginRender" + ], + "summary": "GetRenderConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plugin.RenderConfig" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/report": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add report \u003cbr\u003e source (question, answer, comment, user)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "add report", + "parameters": [ + { + "description": "report", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/report/review": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "review report", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "review report", + "parameters": [ + { + "description": "flag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReviewReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/report/unreviewed/post": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed report post page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "get unreviewed report post page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReportListPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/review/pending/post": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update review", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Review" + ], + "summary": "update review", + "parameters": [ + { + "description": "review", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateReviewReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/review/pending/post/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed post page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Review" + ], + "summary": "get unreviewed post page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/reviewing/type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get reviewing type", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get reviewing type", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReviewingTypeResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions": { + "get": { + "description": "get revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get revision list", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "search object", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "search object", + "parameters": [ + { + "type": "string", + "description": "query string", + "name": "q", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "active", + "score", + "relevance" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SearchResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search/desc": { + "get": { + "description": "get search description", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "get search description", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SearchResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/siteinfo": { + "get": { + "description": "get site info", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/siteinfo/legal": { + "get": { + "description": "get site legal info", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site legal info", + "parameters": [ + { + "enum": [ + "tos", + "privacy" + ], + "type": "string", + "description": "legal information type", + "name": "info_type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetSiteLegalInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tag": { + "get": { + "description": "get tag one", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag one", + "parameters": [ + { + "type": "string", + "description": "tag id", + "name": "tag_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tag name", + "name": "tag_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetTagResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "update tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "add tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "delete tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RemoveTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/merge": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "merge tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "merge tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover delete tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "recover delete tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RecoverTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/synonym": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "update tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateTagSynonymReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/synonyms": { + "get": { + "description": "get tag synonyms", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag synonyms", + "parameters": [ + { + "type": "integer", + "description": "tag id", + "name": "tag_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetTagSynonymsResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags": { + "get": { + "description": "get tags list by slug name", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tags list", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "string collection", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags/following": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get following tag list", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get following tag list", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetFollowingTagsResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags/page": { + "get": { + "description": "get tag page", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag page", + "parameters": [ + { + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "slug_name", + "name": "slug_name", + "in": "query" + }, + { + "enum": [ + "popular", + "name", + "newest" + ], + "type": "string", + "description": "query condition", + "name": "query_cond", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/action/record": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "ActionRecord", + "tags": [ + "User" + ], + "summary": "ActionRecord", + "parameters": [ + { + "enum": [ + "login", + "e_mail", + "find_pass" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.ActionRecordResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/email": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user change email verification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user change email verification", + "parameters": [ + { + "description": "UserChangeEmailVerifyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/email/change/code": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "send email to the user email then change their email", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send email to the user email then change their email", + "parameters": [ + { + "description": "UserChangeEmailSendCodeReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/email/verification": { + "post": { + "description": "UserVerifyEmail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserVerifyEmail", + "parameters": [ + { + "type": "string", + "default": "", + "description": "code", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/email/verification/send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserVerifyEmailSend", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserVerifyEmailSend", + "parameters": [ + { + "type": "string", + "default": "", + "description": "captcha_id", + "name": "captcha_id", + "in": "query" + }, + { + "type": "string", + "default": "", + "description": "captcha_code", + "name": "captcha_code", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user info, if user no login response http code is 200, but user info is null", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "GetUserInfoByUserID", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetCurrentLoginUserInfoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInfo update user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInfo update user info", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateInfoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/info/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "SearchUserListByName", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "SearchUserListByName", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetOtherUserInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/login/email": { + "post": { + "description": "UserEmailLogin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserEmailLogin", + "parameters": [ + { + "description": "UserEmailLogin", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserEmailLoginReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user logout", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/notification/config": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "update user's notification config", + "parameters": [ + { + "description": "UpdateUserNotificationConfigReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user's notification config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserNotificationConfigResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/notification/unsubscribe": { + "put": { + "description": "unsubscribe notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "unsubscribe notification", + "parameters": [ + { + "description": "UserUnsubscribeNotificationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserUnsubscribeNotificationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/password": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserModifyPassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserModifyPassWord", + "parameters": [ + { + "description": "UserModifyPasswordReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserModifyPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/password/replacement": { + "post": { + "description": "UseRePassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UseRePassWord", + "parameters": [ + { + "description": "UserRePassWordRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRePassWordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/password/reset": { + "post": { + "description": "RetrievePassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "RetrievePassWord", + "parameters": [ + { + "description": "UserRetrievePassWordRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRetrievePassWordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/plugin/config": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user plugin config", + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "get user plugin config", + "parameters": [ + { + "type": "string", + "description": "plugin_slug_name", + "name": "plugin_slug_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetPluginConfigResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user plugin config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "update user plugin config", + "parameters": [ + { + "description": "UpdatePluginConfigReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPluginConfigReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/plugin/configs": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get plugin list that used for user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "get plugin list that used for user.", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserPluginListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/ranking": { + "get": { + "description": "get user ranking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user ranking", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserRankingResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/register/email": { + "post": { + "description": "UserRegisterByEmail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserRegisterByEmail", + "parameters": [ + { + "description": "UserRegisterReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRegisterReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/staff": { + "get": { + "description": "get user staff", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user staff", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserStaffResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/vote/down": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add vote", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Activity" + ], + "summary": "vote down", + "parameters": [ + { + "description": "vote", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.VoteReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.VoteResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/vote/up": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add vote", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Activity" + ], + "summary": "vote up", + "parameters": [ + { + "description": "vote", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.VoteReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.VoteResp" + } + } + } + ] + } + } + } + } + }, + "/custom.css": { + "get": { + "description": "get site custom CSS", + "produces": [ + "text/css" + ], + "tags": [ + "site" + ], + "summary": "get site custom CSS", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/installation/base-info": { + "post": { + "description": "init base info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "init base info", + "parameters": [ + { + "description": "InitBaseInfoReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.InitBaseInfoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/config-file/check": { + "post": { + "description": "check config file if exist when installation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "check config file if exist when installation", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/install.CheckConfigFileResp" + } + } + } + ] + } + } + } + } + }, + "/installation/db/check": { + "post": { + "description": "check database if exist when installation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "check database if exist when installation", + "parameters": [ + { + "description": "CheckDatabaseReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.CheckDatabaseReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/install.CheckConfigFileResp" + } + } + } + ] + } + } + } + } + }, + "/installation/init": { + "post": { + "description": "init environment", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "init environment", + "parameters": [ + { + "description": "CheckDatabaseReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.CheckDatabaseReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/language/config": { + "get": { + "description": "get installation language config mapping", + "produces": [ + "application/json" + ], + "tags": [ + "Lang" + ], + "summary": "get installation language config mapping", + "parameters": [ + { + "type": "string", + "description": "installation language", + "name": "lang", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/language/options": { + "get": { + "description": "get installation language options", + "produces": [ + "application/json" + ], + "tags": [ + "Lang" + ], + "summary": "get installation language options", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/translator.LangOption" + } + } + } + } + ] + } + } + } + } + }, + "/personal/question/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list personal questions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Personal" + ], + "summary": "list personal questions", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "score" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/robots.txt": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + } }, "definitions": { - "entity.AdminSetAnswerStatusRequest": { + "constant.NotificationChannelKey": { + "type": "string", + "enum": [ + "email" + ], + "x-enum-varnames": [ + "EmailChannel" + ] + }, + "constant.Privilege": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "integer", + "minimum": 1 + } + } + }, + "entity.BadgeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BadgeLevelBronze", + "BadgeLevelSilver", + "BadgeLevelGold" + ] + }, + "handler.RespBody": { + "type": "object", + "properties": { + "code": { + "description": "http code", + "type": "integer" + }, + "data": { + "description": "response data" + }, + "msg": { + "description": "response message", + "type": "string" + }, + "reason": { + "description": "reason key", + "type": "string" + } + } + }, + "install.CheckConfigFileResp": { + "type": "object", + "properties": { + "config_file_exist": { + "type": "boolean" + }, + "db_connection_success": { + "type": "boolean" + }, + "db_table_exist": { + "type": "boolean" + } + } + }, + "install.CheckDatabaseReq": { + "type": "object", + "required": [ + "db_type" + ], + "properties": { + "db_file": { + "type": "string" + }, + "db_host": { + "type": "string" + }, + "db_name": { + "type": "string" + }, + "db_password": { + "type": "string" + }, + "db_type": { + "type": "string", + "enum": [ + "postgres", + "sqlite3", + "mysql" + ] + }, + "db_username": { + "type": "string" + }, + "ssl_cert": { + "type": "string" + }, + "ssl_enabled": { + "type": "boolean" + }, + "ssl_key": { + "type": "string" + }, + "ssl_mode": { + "type": "string" + }, + "ssl_root_cert": { + "type": "string" + } + } + }, + "install.InitBaseInfoReq": { + "type": "object", + "required": [ + "contact_email", + "email", + "external_content_display", + "lang", + "name", + "password", + "site_name", + "site_url" + ], + "properties": { + "contact_email": { + "type": "string", + "maxLength": 500 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "lang": { + "type": "string", + "maxLength": 30 + }, + "login_required": { + "type": "boolean" + }, + "name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "site_name": { + "type": "string", + "maxLength": 30 + }, + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "pager.PageModel": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "list": {} + } + }, + "plugin.EmbedConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "platform": { + "type": "string" + } + } + }, + "plugin.RenderConfig": { + "type": "object", + "properties": { + "select_theme": { + "type": "string" + } + } + }, + "schema.AcceptAnswerReq": { + "type": "object", + "required": [ + "question_id" + ], + "properties": { + "answer_id": { + "type": "string" + }, + "question_id": { + "type": "string", + "maxLength": 30 + } + } + }, + "schema.ActObjectInfo": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "main_tag_slug_name": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "schema.ActObjectTimeline": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + }, + "activity_type": { + "type": "string" + }, + "cancelled": { + "type": "boolean" + }, + "cancelled_at": { + "type": "integer" + }, + "comment": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + } + } + }, + "schema.ActionRecordResp": { + "type": "object", + "properties": { + "captcha_id": { + "type": "string" + }, + "captcha_img": { + "type": "string" + }, + "verify": { + "type": "boolean" + } + } + }, + "schema.AddCommentReq": { + "type": "object", + "required": [ + "object_id", + "original_text" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "mention_username_list": { + "description": "@ user id list", + "type": "array", + "items": { + "type": "string" + } + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "original_text": { + "description": "original comment content", + "type": "string", + "maxLength": 600, + "minLength": 2 + }, + "reply_comment_id": { + "description": "reply comment id", + "type": "string" + } + } + }, + "schema.AddReportReq": { + "type": "object", + "required": [ + "object_id", + "report_type" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, + "content": { + "description": "report content", + "type": "string", + "maxLength": 500 + }, + "object_id": { + "description": "object id", + "type": "string", + "maxLength": 20 + }, + "report_type": { + "description": "report type", + "type": "integer" + } + } + }, + "schema.AddTagReq": { + "type": "object", + "required": [ + "display_name", + "original_text", + "slug_name" + ], + "properties": { + "display_name": { + "description": "display_name", + "type": "string", + "maxLength": 35 + }, + "original_text": { + "description": "original text", + "type": "string", + "maxLength": 65536 + }, + "slug_name": { + "description": "slug_name", + "type": "string", + "maxLength": 35 + } + } + }, + "schema.AddUserReq": { + "type": "object", + "required": [ + "display_name", + "email", + "password" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.AddUsersReq": { + "type": "object", + "properties": { + "users": { + "description": "users info line by line", + "type": "string" + } + } + }, + "schema.AdminUpdateAnswerStatusReq": { + "type": "object", + "required": [ + "answer_id", + "status" + ], + "properties": { + "answer_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "available", + "deleted" + ] + } + } + }, + "schema.AdminUpdateQuestionStatusReq": { + "type": "object", + "required": [ + "question_id", + "status" + ], + "properties": { + "question_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "available", + "closed", + "deleted" + ] + } + } + }, + "schema.AnswerAddReq": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "question_id": { + "type": "string" + } + } + }, + "schema.AnswerInfo": { + "type": "object", + "properties": { + "accepted": { + "type": "integer" + }, + "collected": { + "type": "boolean" + }, + "content": { + "type": "string" + }, + "create_time": { + "type": "integer" + }, + "html": { + "type": "string" + }, + "id": { + "type": "string" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "question_id": { + "type": "string" + }, + "question_info": { + "$ref": "#/definitions/schema.QuestionInfoResp" + }, + "status": { + "type": "integer" + }, + "update_time": { + "type": "integer" + }, + "update_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "vote_count": { + "type": "integer" + }, + "vote_status": { + "type": "string" + } + } + }, + "schema.AnswerUpdateReq": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "edit_summary": { + "type": "string" + }, + "id": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "schema.AvatarInfo": { + "type": "object", + "properties": { + "custom": { + "type": "string", + "maxLength": 200 + }, + "gravatar": { + "type": "string", + "maxLength": 200 + }, + "type": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.BadgeListInfo": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "earned_count": { + "description": "badge earned count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.BadgeStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ], + "x-enum-varnames": [ + "BadgeStatusActive", + "BadgeStatusInactive" + ] + }, + "schema.CloseQuestionReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "close_msg": { + "description": "close_type", + "type": "string" + }, + "close_type": { + "description": "close_type", + "type": "integer" + }, + "id": { + "type": "string" + } + } + }, + "schema.CollectionSwitchReq": { + "type": "object", + "required": [ + "group_id", + "object_id" + ], + "properties": { + "bookmark": { + "type": "boolean" + }, + "group_id": { + "type": "string" + }, + "object_id": { + "type": "string" + } + } + }, + "schema.CollectionSwitchResp": { + "type": "object", + "properties": { + "object_collection_count": { + "type": "integer" + } + } + }, + "schema.ConfigField": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ConfigFieldOption" + } + }, + "required": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "ui_options": { + "$ref": "#/definitions/schema.ConfigFieldUIOptions" + }, + "value": {} + } + }, + "schema.ConfigFieldOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "schema.ConfigFieldUIOptions": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/schema.UIOptionAction" + }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, + "input_type": { + "type": "string" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "rows": { + "type": "string" + }, + "text": { + "type": "string" + }, + "variant": { + "type": "string" + } + } + }, + "schema.ConnectorInfoResp": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "schema.ConnectorUserInfoResp": { + "type": "object", + "properties": { + "binding": { + "type": "boolean" + }, + "external_id": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, + "schema.EditUserProfileReq": { + "type": "object", + "required": [ + "display_name", + "email", + "user_id" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string", + "maxLength": 30, + "minLength": 2 + } + } + }, + "schema.ExternalLoginBindingUserSendEmailReq": { + "type": "object", + "required": [ + "binding_key", + "email" + ], + "properties": { + "binding_key": { + "type": "string", + "maxLength": 100 + }, + "email": { + "type": "string", + "maxLength": 512 + }, + "must": { + "description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.", + "type": "boolean" + } + } + }, + "schema.ExternalLoginBindingUserSendEmailResp": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "email_exist_and_must_be_confirmed": { + "type": "boolean" + } + } + }, + "schema.ExternalLoginUnbindingReq": { + "type": "object", + "required": [ + "external_id" + ], + "properties": { + "external_id": { + "type": "string", + "maxLength": 128 + } + } + }, + "schema.FollowReq": { + "type": "object", + "required": [ + "object_id" + ], + "properties": { + "is_cancel": { + "description": "is cancel", + "type": "boolean" + }, + "object_id": { + "description": "object id", + "type": "string" + } + } + }, + "schema.FollowResp": { + "type": "object", + "properties": { + "follows": { + "description": "the followers of object", + "type": "integer" + }, + "is_followed": { + "description": "if user is followed object will be true,otherwise false", + "type": "boolean" + } + } + }, + "schema.GetAnswerInfoResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.AnswerInfo" + }, + "question": { + "$ref": "#/definitions/schema.QuestionInfoResp" + } + } + }, + "schema.GetBadgeInfoResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned_count": { + "description": "badge earned count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "is_single": { + "description": "badge is single or multiple", + "type": "boolean" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.GetBadgeListPagedResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned": { + "description": "badge earned count", + "type": "boolean" + }, + "group_name": { + "description": "badge group name", + "type": "string" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, + "schema.GetBadgeListResp": { + "type": "object", + "properties": { + "badges": { + "description": "badge list info", + "type": "array", + "items": { + "$ref": "#/definitions/schema.BadgeListInfo" + } + }, + "group_name": { + "description": "badge group name", + "type": "string" + } + } + }, + "schema.GetCommentPersonalWithPageResp": { + "type": "object", + "properties": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "comment_id": { + "description": "comment id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + } + } + }, + "schema.GetCommentResp": { + "type": "object", + "properties": { + "comment_id": { + "description": "comment id", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "is_vote": { + "description": "current user if already vote this comment", + "type": "boolean" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "original_text": { + "description": "original comment content", + "type": "string" + }, + "parsed_text": { + "description": "parsed comment content", + "type": "string" + }, + "reply_comment_id": { + "description": "reply comment id", + "type": "string" + }, + "reply_user_display_name": { + "description": "reply user display name", + "type": "string" + }, + "reply_user_id": { + "description": "reply user id", + "type": "string" + }, + "reply_user_status": { + "description": "reply user status", + "type": "string" + }, + "reply_username": { + "description": "reply user username", + "type": "string" + }, + "user_avatar": { + "description": "user avatar", + "type": "string" + }, + "user_display_name": { + "description": "user display name", + "type": "string" + }, + "user_id": { + "description": "user id", + "type": "string" + }, + "user_status": { + "description": "user status", + "type": "string" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "user vote amount", + "type": "integer" + } + } + }, + "schema.GetCurrentLoginUserInfoResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "$ref": "#/definitions/schema.AvatarInfo" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "have_password": { + "description": "user have password", + "type": "boolean" + }, + "id": { + "description": "user id", + "type": "string" + }, + "language": { + "description": "language", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "visit_token": { + "description": "visit token", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, + "schema.GetFollowingTagsResp": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.GetObjectTimelineResp": { + "type": "object", + "properties": { + "object_info": { + "$ref": "#/definitions/schema.ActObjectInfo" + }, + "timeline": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ActObjectTimeline" + } + } + } + }, + "schema.GetOtherUserInfoByUsernameResp": { + "type": "object", + "properties": { + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "avatar": { + "description": "avatar", + "type": "string" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "follow_count": { + "description": "email\nfollow count", + "type": "integer" + }, + "id": { + "description": "user id", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "status": { + "type": "string" + }, + "status_msg": { + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, + "schema.GetOtherUserInfoResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" + } + } + }, + "schema.GetPluginConfigResp": { + "type": "object", + "properties": { + "config_fields": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ConfigField" + } + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug_name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "schema.GetPluginListResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "have_config": { + "type": "boolean" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug_name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "schema.GetPrivilegesConfigResp": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PrivilegeOption" + } + }, + "selected_level": { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + } + }, + "schema.GetRankPersonalPageResp": { + "type": "object", + "properties": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "rank_type": { + "description": "rank type", + "type": "string" + }, + "reputation": { + "description": "reputation", + "type": "integer" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + } + } + }, + "schema.GetReportListPageResp": { + "type": "object", + "properties": { + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" + }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "flag_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/schema.ReasonItem" + }, + "reason_content": { + "type": "string" + }, + "submit_at": { + "type": "integer" + }, + "submitter_user": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetReviewingTypeResp": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "todo_amount": { + "type": "integer" + } + } + }, + "schema.GetRevisionResp": { + "type": "object", + "properties": { + "content": {}, + "create_at": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + }, + "use_id": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + } + } + }, + "schema.GetRoleResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "schema.GetSMTPConfigResp": { + "type": "object", + "properties": { + "encryption": { + "description": "\"\" SSL TLS", + "type": "string" + }, + "from_email": { + "type": "string" + }, + "from_name": { + "type": "string" + }, + "smtp_authentication": { + "type": "boolean" + }, + "smtp_host": { + "type": "string" + }, + "smtp_password": { + "type": "string" + }, + "smtp_port": { + "type": "integer" + }, + "smtp_username": { + "type": "string" + } + } + }, + "schema.GetSiteLegalInfoResp": { + "type": "object", + "properties": { + "privacy_policy_original_text": { + "type": "string" + }, + "privacy_policy_parsed_text": { + "type": "string" + }, + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { + "type": "string" + } + } + }, + "schema.GetTagBasicResp": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + }, + "tag_id": { + "type": "string" + } + } + }, + "schema.GetTagPageResp": { + "type": "object", + "properties": { + "created_at": { + "description": "created time", + "type": "integer" + }, + "description": { + "description": "description", + "type": "string" + }, + "display_name": { + "description": "display_name", + "type": "string" + }, + "excerpt": { + "description": "excerpt", + "type": "string" + }, + "follow_count": { + "description": "follower amount", + "type": "integer" + }, + "is_follower": { + "description": "is follower", + "type": "boolean" + }, + "original_text": { + "description": "original text", + "type": "string" + }, + "parsed_text": { + "description": "parsed_text", + "type": "string" + }, + "question_count": { + "description": "question amount", + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "description": "slug_name", + "type": "string" + }, + "tag_id": { + "description": "tag_id", + "type": "string" + }, + "updated_at": { + "description": "updated time", + "type": "integer" + } + } + }, + "schema.GetTagResp": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "excerpt": { + "type": "string" + }, + "follow_count": { + "type": "integer" + }, + "is_follower": { + "type": "boolean" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "member_actions": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_count": { + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tag_id": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "schema.GetTagSynonymsResp": { + "type": "object", + "properties": { + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "synonyms": { + "description": "synonyms", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagSynonym" + } + } + } + }, + "schema.GetUnreviewedPostPageResp": { "type": "object", "properties": { "answer_id": { "type": "string" }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "review_id": { + "type": "integer" + }, + "submit_at": { + "type": "integer" + }, + "submitter_display_name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetUnreviewedRevisionResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" + }, + "type": { + "type": "string" + }, + "unreviewed_info": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + }, + "schema.GetUserActivationResp": { + "type": "object", + "properties": { + "activation_url": { + "type": "string" + } + } + }, + "schema.GetUserBadgeAwardListResp": { + "type": "object", + "properties": { + "earned_count": { + "description": "badge award count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.GetUserNotificationConfigResp": { + "type": "object", + "properties": { + "all_new_question": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "all_new_question_for_following_tags": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "inbox": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + }, + "schema.GetUserPageResp": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "deleted_at": { + "description": "delete time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "role_name": { + "description": "role name", + "type": "string" + }, "status": { + "description": "user status(normal,suspended,deleted,inactive)", + "type": "string" + }, + "suspended_at": { + "description": "suspended time", + "type": "integer" + }, + "suspended_until": { + "description": "suspended until time", + "type": "integer" + }, + "user_id": { + "description": "user id", + "type": "string" + }, + "username": { + "description": "username", "type": "string" } } }, - "handler.RespBody": { + "schema.GetUserPluginListResp": { "type": "object", "properties": { - "code": { - "description": "http code", - "type": "integer" + "name": { + "type": "string" }, - "data": { - "description": "response data" + "slug_name": { + "type": "string" + } + } + }, + "schema.GetUserStaffResp": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" }, - "msg": { - "description": "response message", + "display_name": { + "description": "display name", "type": "string" }, - "reason": { - "description": "reason key", + "username": { + "description": "username", "type": "string" } } }, - "pager.PageModel": { + "schema.GetVoteWithPageResp": { "type": "object", "properties": { - "count": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", "type": "integer" }, - "list": {} + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + }, + "vote_type": { + "description": "vote type", + "type": "string" + } } }, - "schema.ActionRecordResp": { + "schema.LoadingAction": { "type": "object", "properties": { - "captcha_id": { + "state": { "type": "string" }, - "captcha_img": { + "text": { "type": "string" - }, - "verify": { + } + } + }, + "schema.NotificationChannelConfig": { + "type": "object", + "properties": { + "enable": { "type": "boolean" + }, + "key": { + "$ref": "#/definitions/constant.NotificationChannelKey" } } }, - "schema.AddCommentReq": { + "schema.NotificationClearIDRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "schema.NotificationClearRequest": { "type": "object", "required": [ - "object_id", - "original_text", - "parsed_text" + "type" ], "properties": { - "mention_username_list": { - "description": "@ user id list", - "type": "array", - "items": { - "type": "string" - } + "type": { + "type": "string", + "enum": [ + "inbox", + "achievement" + ] + } + } + }, + "schema.OnCompleteAction": { + "type": "object", + "properties": { + "refresh_form_config": { + "type": "boolean" }, - "object_id": { - "description": "object id", + "toast_return_message": { + "type": "boolean" + } + } + }, + "schema.Operation": { + "type": "object", + "properties": { + "description": { "type": "string" }, - "original_text": { - "description": "original comment content", - "type": "string" + "level": { + "$ref": "#/definitions/schema.OperationLevel" }, - "parsed_text": { - "description": "parsed comment content", + "msg": { "type": "string" }, - "reply_comment_id": { - "description": "reply comment id", + "time": { + "type": "integer" + }, + "type": { "type": "string" } } }, - "schema.AddReportReq": { + "schema.OperationLevel": { + "type": "string", + "enum": [ + "info", + "danger", + "warning", + "secondary" + ], + "x-enum-varnames": [ + "OperationLevelInfo", + "OperationLevelDanger", + "OperationLevelWarning", + "OperationLevelSecondary" + ] + }, + "schema.OperationQuestionReq": { "type": "object", "required": [ - "object_id", - "report_type" + "id" ], "properties": { - "content": { - "description": "report content", - "type": "string", - "maxLength": 500 + "id": { + "type": "string" }, - "object_id": { - "description": "object id", - "type": "string", - "maxLength": 20 + "operation": { + "description": "operation [pin unpin hide show]", + "type": "string" + } + } + }, + "schema.PermissionMemberAction": { + "type": "object", + "properties": { + "action": { + "type": "string" }, - "report_type": { - "description": "report type", - "type": "integer" + "name": { + "type": "string" + }, + "type": { + "type": "string" } } }, - "schema.AdminSetQuestionStatusRequest": { + "schema.PostRenderReq": { "type": "object", "properties": { - "question_id": { + "content": { "type": "string" + } + } + }, + "schema.PrivilegeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 99 + ], + "x-enum-varnames": [ + "PrivilegeLevel1", + "PrivilegeLevel2", + "PrivilegeLevel3", + "PrivilegeLevelCustom" + ] + }, + "schema.PrivilegeOption": { + "type": "object", + "properties": { + "level": { + "$ref": "#/definitions/schema.PrivilegeLevel" }, - "status": { + "level_desc": { "type": "string" + }, + "privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } } } }, - "schema.AnswerAddReq": { + "schema.QuestionAdd": { "type": "object", + "required": [ + "content", + "tags", + "title" + ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, "content": { "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, + "schema.QuestionAddByAnswer": { + "type": "object", + "required": [ + "answer_content", + "content", + "tags", + "title" + ], + "properties": { + "answer_content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "captcha_code": { "type": "string" }, - "html": { - "description": "html", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "question_id": { - "description": "question_id", - "type": "string" - } - } - }, - "schema.AnswerAdoptedReq": { - "type": "object", - "properties": { - "answer_id": { - "type": "string" + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "mention_username_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } }, - "question_id": { - "description": "question_id", - "type": "string" + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, - "schema.AnswerList": { + "schema.QuestionInfoResp": { "type": "object", "properties": { - "order": { - "description": "1 Default 2 time", + "accepted_answer_id": { "type": "string" }, - "page": { - "description": "Query number of pages", + "answer_count": { "type": "integer" }, - "page_size": { - "description": "Search page size", + "answered": { + "type": "boolean" + }, + "collected": { + "type": "boolean" + }, + "collection_count": { "type": "integer" }, - "question_id": { - "description": "question_id", - "type": "string" - } - } - }, - "schema.AnswerUpdateReq": { - "type": "object", - "properties": { "content": { - "description": "content", "type": "string" }, - "edit_summary": { - "description": "edit_summary", + "create_time": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "edit_time": { + "type": "integer" + }, + "extends_actions": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "first_answer_id": { "type": "string" }, + "follow_count": { + "type": "integer" + }, "html": { - "description": "html", "type": "string" }, "id": { - "description": "id", "type": "string" }, - "question_id": { - "description": "question_id", - "type": "string" + "is_followed": { + "type": "boolean" }, - "title": { - "description": "title", - "type": "string" - } - } - }, - "schema.CloseQuestionReq": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "close_msg": { - "description": "close_type", + "last_answer_id": { "type": "string" }, - "close_type": { - "description": "close_type", + "last_answered_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "operation": { + "$ref": "#/definitions/schema.Operation" + }, + "pin": { "type": "integer" }, - "id": { - "type": "string" - } - } - }, - "schema.CollectionSwitchReq": { - "type": "object", - "required": [ - "group_id", - "object_id" - ], - "properties": { - "group_id": { - "description": "user collection group TagID", - "type": "string" + "show": { + "type": "integer" }, - "object_id": { - "description": "object TagID", - "type": "string" - } - } - }, - "schema.CollectionSwitchResp": { - "type": "object", - "properties": { - "object_collection_count": { - "type": "string" + "status": { + "type": "integer" }, - "object_id": { + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { "type": "string" }, - "switch": { - "type": "boolean" - } - } - }, - "schema.FollowReq": { - "type": "object", - "required": [ - "object_id" - ], - "properties": { - "is_cancel": { - "description": "is cancel", - "type": "boolean" + "unique_view_count": { + "type": "integer" }, - "object_id": { - "description": "object id", - "type": "string" - } - } - }, - "schema.FollowResp": { - "type": "object", - "properties": { - "follows": { - "description": "the followers of object", + "update_time": { "type": "integer" }, - "is_followed": { - "description": "if user is followed object will be true,otherwise false", - "type": "boolean" - } - } - }, - "schema.GetCommentPersonalWithPageResp": { - "type": "object", - "properties": { - "answer_id": { - "description": "answer id", - "type": "string" + "update_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" }, - "comment_id": { - "description": "comment id", + "url_title": { "type": "string" }, - "content": { - "description": "content", - "type": "string" + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" }, - "created_at": { - "description": "create time", + "view_count": { "type": "integer" }, - "object_id": { - "description": "object id", + "vote_count": { + "type": "integer" + }, + "vote_status": { "type": "string" + } + } + }, + "schema.QuestionPageReq": { + "type": "object", + "properties": { + "in_days": { + "type": "integer", + "minimum": 1 }, - "object_type": { - "description": "object type", + "order": { "type": "string", "enum": [ - "question", - "answer", - "tag", - "comment" + "newest", + "active", + "hot", + "score", + "unanswered", + "recommend", + "frequent" ] }, - "question_id": { - "description": "question id", - "type": "string" + "page": { + "type": "integer", + "minimum": 1 }, - "title": { - "description": "title", - "type": "string" + "page_size": { + "type": "integer", + "minimum": 1 + }, + "tag": { + "type": "string", + "maxLength": 100 + }, + "username": { + "type": "string", + "maxLength": 100 } } }, - "schema.GetCommentResp": { + "schema.QuestionPageResp": { "type": "object", "properties": { - "comment_id": { - "description": "comment id", + "accepted_answer_id": { + "description": "answer information", "type": "string" }, - "created_at": { - "description": "create time", + "answer_count": { "type": "integer" }, - "is_vote": { - "description": "current user if already vote this comment", - "type": "boolean" + "collection_count": { + "type": "integer" }, - "member_actions": { - "description": "MemberActions", - "type": "array", - "items": { - "$ref": "#/definitions/schema.PermissionMemberAction" - } + "created_at": { + "type": "integer" }, - "object_id": { - "description": "object id", + "description": { "type": "string" }, - "original_text": { - "description": "original comment content", - "type": "string" + "follow_count": { + "type": "integer" }, - "parsed_text": { - "description": "parsed comment content", + "id": { "type": "string" }, - "reply_comment_id": { - "description": "reply comment id", + "last_answer_id": { "type": "string" }, - "reply_user_display_name": { - "description": "reply user display name", - "type": "string" + "operated_at": { + "description": "operator information", + "type": "integer" }, - "reply_user_id": { - "description": "reply user id", + "operation_type": { "type": "string" }, - "reply_user_status": { - "description": "reply user status", - "type": "string" + "operator": { + "$ref": "#/definitions/schema.QuestionPageRespOperator" }, - "reply_username": { - "description": "reply user username", - "type": "string" + "pin": { + "description": "1: unpin, 2: pin", + "type": "integer" + }, + "show": { + "description": "0: show, 1: hide", + "type": "integer" + }, + "status": { + "type": "integer" }, - "user_avatar": { - "description": "user avatar", - "type": "string" + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } }, - "user_display_name": { - "description": "user display name", + "title": { "type": "string" }, - "user_id": { - "description": "user id", - "type": "string" + "unique_view_count": { + "type": "integer" }, - "user_status": { - "description": "user status", + "url_title": { "type": "string" }, - "username": { - "description": "username", - "type": "string" + "view_count": { + "description": "question statistical information", + "type": "integer" }, "vote_count": { - "description": "user vote amount", "type": "integer" } } }, - "schema.GetFollowingTagsResp": { + "schema.QuestionPageRespOperator": { "type": "object", "properties": { + "avatar": { + "type": "string" + }, "display_name": { - "description": "display name", "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "id": { "type": "string" }, - "slug_name": { - "description": "slug name", + "rank": { + "type": "integer" + }, + "status": { "type": "string" }, - "tag_id": { - "description": "tag id", + "username": { "type": "string" } } }, - "schema.GetOtherUserInfoByUsernameResp": { + "schema.QuestionRecoverReq": { "type": "object", + "required": [ + "question_id" + ], "properties": { - "answer_count": { - "description": "answer count", - "type": "integer" + "question_id": { + "type": "string" + } + } + }, + "schema.QuestionUpdate": { + "type": "object", + "required": [ + "content", + "id", + "tags", + "title" + ], + "properties": { + "captcha_code": { + "type": "string" }, - "avatar": { - "description": "avatar", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "bio": { - "description": "bio markdown", + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "edit_summary": { + "description": "edit summary", "type": "string" }, - "bio_html": { - "description": "bio html", + "id": { + "description": "question id", "type": "string" }, - "created_at": { - "description": "create time", - "type": "integer" + "invite_user": { + "type": "array", + "items": { + "type": "string" + } }, - "display_name": { - "description": "display name", + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, + "schema.QuestionUpdateInviteUser": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "captcha_code": { "type": "string" }, - "follow_count": { - "description": "email\nfollow count", - "type": "integer" + "captcha_id": { + "description": "captcha_id", + "type": "string" }, "id": { - "description": "user id", "type": "string" }, - "ip_info": { - "description": "ip info", + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "schema.ReactionRespItem": { + "type": "object", + "properties": { + "count": { + "description": "Count is the number of users who reacted", + "type": "integer" + }, + "emoji": { + "description": "Emoji is the reaction emoji", "type": "string" }, - "is_admin": { - "description": "is admin", + "is_active": { + "description": "IsActive is if current user has reacted", "type": "boolean" }, - "last_login_date": { - "description": "last login date", - "type": "integer" - }, - "location": { - "description": "location", + "tooltip": { + "description": "Tooltip is the user's name who reacted", "type": "string" - }, - "mobile": { - "description": "mobile", + } + } + }, + "schema.ReasonItem": { + "type": "object", + "properties": { + "content_type": { "type": "string" }, - "question_count": { - "description": "question count", - "type": "integer" - }, - "rank": { - "description": "rank", - "type": "integer" + "description": { + "type": "string" }, - "status": { + "name": { "type": "string" }, - "status_msg": { + "placeholder": { "type": "string" }, - "username": { - "description": "username", + "reason_key": { "type": "string" }, - "website": { - "description": "website", + "reason_type": { + "type": "integer" + } + } + }, + "schema.RecoverAnswerReq": { + "type": "object", + "required": [ + "answer_id" + ], + "properties": { + "answer_id": { "type": "string" } } }, - "schema.GetOtherUserInfoResp": { + "schema.RecoverTagReq": { "type": "object", + "required": [ + "tag_id" + ], "properties": { - "has": { - "type": "boolean" - }, - "info": { - "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" + "tag_id": { + "type": "string" } } }, - "schema.GetRankPersonalWithPageResp": { + "schema.RemoveAnswerReq": { "type": "object", + "required": [ + "id" + ], "properties": { - "answer_id": { - "description": "answer id", + "captcha_code": { "type": "string" }, - "content": { - "description": "content", + "captcha_id": { "type": "string" }, - "created_at": { - "description": "create time", - "type": "integer" - }, - "object_id": { - "description": "object id", + "id": { "type": "string" - }, - "object_type": { - "description": "object type", - "type": "string", - "enum": [ - "question", - "answer", - "tag", - "comment" - ] - }, - "question_id": { - "description": "question id", + } + } + }, + "schema.RemoveCommentReq": { + "type": "object", + "required": [ + "comment_id" + ], + "properties": { + "captcha_code": { "type": "string" }, - "rank_type": { - "description": "rank type", + "captcha_id": { "type": "string" }, - "reputation": { - "description": "reputation", - "type": "integer" - }, - "title": { - "description": "title", + "comment_id": { + "description": "comment id", "type": "string" } } }, - "schema.GetReportTypeResp": { + "schema.RemoveQuestionReq": { "type": "object", + "required": [ + "id" + ], "properties": { - "content_type": { - "description": "content type", + "captcha_code": { "type": "string" }, - "description": { - "description": "report description", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "have_content": { - "description": "is have content", - "type": "boolean" - }, - "name": { - "description": "report name", + "id": { + "description": "question id", + "type": "string" + } + } + }, + "schema.RemoveTagReq": { + "type": "object", + "required": [ + "tag_id" + ], + "properties": { + "tag_id": { + "description": "tag_id", "type": "string" - }, - "source": { - "description": "report source", + } + } + }, + "schema.ReopenQuestionReq": { + "type": "object", + "properties": { + "question_id": { "type": "string" - }, - "type": { - "description": "report type", - "type": "integer" } } }, - "schema.GetRevisionResp": { + "schema.ReviewReportReq": { "type": "object", + "required": [ + "flag_id", + "operation_type" + ], "properties": { - "content": { - "description": "content parsed" + "close_msg": { + "type": "string" }, - "create_at": { + "close_type": { "type": "integer" }, - "id": { - "description": "id", - "type": "string" + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 }, - "object_id": { - "description": "object id", + "flag_id": { "type": "string" }, - "reason": { - "type": "string" + "operation_type": { + "type": "string", + "enum": [ + "edit_post", + "close_post", + "delete_post", + "unlist_post", + "ignore_report" + ] }, - "status": { - "description": "revision status(normal: 1; delete 2)", - "type": "integer" + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } }, "title": { - "description": "title", - "type": "string" - }, - "use_id": { - "description": "user id", - "type": "string" - }, - "user_info": { - "$ref": "#/definitions/schema.UserBasicInfo" + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, - "schema.GetSMTPConfigResp": { + "schema.RevisionAuditReq": { "type": "object", + "required": [ + "id", + "operation" + ], "properties": { - "encryption": { - "description": "\"\" SSL", - "type": "string" - }, - "from_email": { - "type": "string" - }, - "from_name": { - "type": "string" - }, - "smtp_authentication": { - "type": "boolean" - }, - "smtp_host": { - "type": "string" - }, - "smtp_password": { + "id": { + "description": "object id", "type": "string" }, - "smtp_port": { - "type": "integer" - }, - "smtp_username": { + "operation": { + "description": "approve or reject", "type": "string" } } }, - "schema.GetTagPageResp": { + "schema.SearchObject": { "type": "object", "properties": { - "created_at": { - "description": "created time", + "accepted": { + "type": "boolean" + }, + "answer_count": { "type": "integer" }, - "display_name": { - "description": "display_name", - "type": "string" + "created_at": { + "type": "integer" }, "excerpt": { - "description": "excerpt", "type": "string" }, - "follow_count": { - "description": "follower amount", - "type": "integer" - }, - "is_follower": { - "description": "is follower", - "type": "boolean" + "id": { + "type": "string" }, - "original_text": { - "description": "original text", + "question_id": { "type": "string" }, - "parsed_text": { - "description": "parsed_text", + "status": { + "description": "Status", "type": "string" }, - "question_count": { - "description": "question amount", - "type": "integer" + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } }, - "slug_name": { - "description": "slug_name", + "title": { "type": "string" }, - "tag_id": { - "description": "tag_id", + "url_title": { "type": "string" }, - "updated_at": { - "description": "updated time", + "user_info": { + "description": "user info", + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObjectUser" + } + ] + }, + "vote_count": { "type": "integer" } } }, - "schema.GetTagResp": { + "schema.SearchObjectUser": { "type": "object", "properties": { - "created_at": { - "description": "created time", - "type": "integer" - }, "display_name": { - "description": "display name", "type": "string" }, - "excerpt": { - "description": "excerpt", + "id": { "type": "string" }, - "follow_count": { - "description": "follower amount", + "rank": { "type": "integer" }, - "is_follower": { - "description": "is follower", - "type": "boolean" + "status": { + "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "username": { "type": "string" + } + } + }, + "schema.SearchResp": { + "type": "object", + "properties": { + "count": { + "type": "integer" }, - "member_actions": { - "description": "MemberActions", + "list": { + "description": "search response", "type": "array", "items": { - "$ref": "#/definitions/schema.PermissionMemberAction" + "$ref": "#/definitions/schema.SearchResult" } + } + } + }, + "schema.SearchResult": { + "type": "object", + "properties": { + "object": { + "description": "this object", + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObject" + } + ] }, - "original_text": { - "description": "original text", + "object_type": { + "description": "object_type", "type": "string" - }, - "parsed_text": { - "description": "parsed text", + } + } + }, + "schema.SendUserActivationReq": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { "type": "string" + } + } + }, + "schema.SiteBrandingReq": { + "type": "object", + "properties": { + "favicon": { + "type": "string", + "maxLength": 512 }, - "question_count": { - "description": "question amount", - "type": "integer" - }, - "slug_name": { - "description": "slug name", - "type": "string" + "logo": { + "type": "string", + "maxLength": 512 }, - "tag_id": { - "description": "tag id", - "type": "string" + "mobile_logo": { + "type": "string", + "maxLength": 512 }, - "updated_at": { - "description": "updated time", - "type": "integer" + "square_icon": { + "type": "string", + "maxLength": 512 } } }, - "schema.GetTagSynonymsResp": { + "schema.SiteBrandingResp": { "type": "object", "properties": { - "display_name": { - "description": "display name", - "type": "string" + "favicon": { + "type": "string", + "maxLength": 512 }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", - "type": "string" + "logo": { + "type": "string", + "maxLength": 512 }, - "slug_name": { - "description": "slug name", - "type": "string" + "mobile_logo": { + "type": "string", + "maxLength": 512 }, - "tag_id": { - "description": "tag id", - "type": "string" + "square_icon": { + "type": "string", + "maxLength": 512 } } }, - "schema.GetUserPageResp": { + "schema.SiteCustomCssHTMLReq": { "type": "object", "properties": { - "avatar": { - "description": "avatar", - "type": "string" - }, - "created_at": { - "description": "create time", - "type": "integer" + "custom_css": { + "type": "string", + "maxLength": 65536 }, - "deleted_at": { - "description": "delete time", - "type": "integer" + "custom_footer": { + "type": "string", + "maxLength": 65536 }, - "display_name": { - "description": "display name", - "type": "string" + "custom_head": { + "type": "string", + "maxLength": 65536 }, - "e_mail": { - "description": "email", - "type": "string" + "custom_header": { + "type": "string", + "maxLength": 65536 }, - "rank": { - "description": "rank", - "type": "integer" + "custom_sidebar": { + "type": "string", + "maxLength": 65536 + } + } + }, + "schema.SiteCustomCssHTMLResp": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 }, - "status": { - "description": "user status(normal,suspended,deleted,inactive)", - "type": "string" + "custom_footer": { + "type": "string", + "maxLength": 65536 }, - "suspended_at": { - "description": "suspended time", - "type": "integer" + "custom_head": { + "type": "string", + "maxLength": 65536 }, - "user_id": { - "description": "user id", - "type": "string" + "custom_header": { + "type": "string", + "maxLength": 65536 }, - "username": { - "description": "username", - "type": "string" + "custom_sidebar": { + "type": "string", + "maxLength": 65536 } } }, - "schema.GetUserResp": { + "schema.SiteGeneralReq": { "type": "object", + "required": [ + "contact_email", + "name", + "site_url" + ], "properties": { - "access_token": { - "description": "access token", - "type": "string" - }, - "answer_count": { - "description": "answer count", - "type": "integer" + "check_update": { + "type": "boolean" }, - "authority_group": { - "description": "authority group", - "type": "integer" + "contact_email": { + "type": "string", + "maxLength": 512 }, - "avatar": { - "description": "avatar", - "type": "string" + "description": { + "type": "string", + "maxLength": 2000 }, - "bio": { - "description": "bio markdown", - "type": "string" + "name": { + "type": "string", + "maxLength": 128 }, - "bio_html": { - "description": "bio html", - "type": "string" + "short_description": { + "type": "string", + "maxLength": 255 }, - "created_at": { - "description": "create time", - "type": "integer" + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "schema.SiteGeneralResp": { + "type": "object", + "required": [ + "contact_email", + "name", + "site_url" + ], + "properties": { + "check_update": { + "type": "boolean" }, - "display_name": { - "description": "display name", - "type": "string" + "contact_email": { + "type": "string", + "maxLength": 512 }, - "e_mail": { - "description": "email", - "type": "string" + "description": { + "type": "string", + "maxLength": 2000 }, - "follow_count": { - "description": "follow count", - "type": "integer" + "name": { + "type": "string", + "maxLength": 128 }, - "id": { - "description": "user id", - "type": "string" + "short_description": { + "type": "string", + "maxLength": 255 }, - "ip_info": { - "description": "ip info", - "type": "string" + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "schema.SiteInfoResp": { + "type": "object", + "properties": { + "branding": { + "$ref": "#/definitions/schema.SiteBrandingResp" }, - "is_admin": { - "description": "is admin", - "type": "boolean" + "custom_css_html": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" }, - "last_login_date": { - "description": "last login date", - "type": "integer" + "general": { + "$ref": "#/definitions/schema.SiteGeneralResp" }, - "location": { - "description": "location", - "type": "string" + "interface": { + "$ref": "#/definitions/schema.SiteInterfaceResp" }, - "mail_status": { - "description": "mail status(1 pass 2 to be verified)", - "type": "integer" + "login": { + "$ref": "#/definitions/schema.SiteLoginResp" }, - "mobile": { - "description": "mobile", + "revision": { "type": "string" }, - "notice_status": { - "description": "notice status(1 on 2off)", - "type": "integer" + "site_legal": { + "$ref": "#/definitions/schema.SiteLegalSimpleResp" }, - "question_count": { - "description": "question count", - "type": "integer" + "site_seo": { + "$ref": "#/definitions/schema.SiteSeoResp" }, - "rank": { - "description": "rank", - "type": "integer" + "site_users": { + "$ref": "#/definitions/schema.SiteUsersResp" }, - "status": { - "description": "user status", - "type": "string" + "site_write": { + "$ref": "#/definitions/schema.SiteWriteResp" }, - "username": { - "description": "username", - "type": "string" + "theme": { + "$ref": "#/definitions/schema.SiteThemeResp" }, - "website": { - "description": "website", + "version": { "type": "string" } } }, - "schema.GetVoteWithPageResp": { + "schema.SiteInterfaceReq": { "type": "object", + "required": [ + "default_avatar", + "language", + "time_zone" + ], "properties": { - "answer_id": { - "description": "answer id", - "type": "string" - }, - "content": { - "description": "content", - "type": "string" - }, - "created_at": { - "description": "create time", - "type": "integer" - }, - "object_id": { - "description": "object id", - "type": "string" - }, - "object_type": { - "description": "object type", + "default_avatar": { "type": "string", "enum": [ - "question", - "answer", - "tag", - "comment" + "system", + "gravatar" ] }, - "question_id": { - "description": "question id", + "gravatar_base_url": { "type": "string" }, - "title": { - "description": "title", - "type": "string" + "language": { + "type": "string", + "maxLength": 128 }, - "vote_type": { - "description": "vote type", - "type": "string" + "time_zone": { + "type": "string", + "maxLength": 128 } } }, - "schema.NotificationClearIDRequest": { + "schema.SiteInterfaceResp": { "type": "object", + "required": [ + "default_avatar", + "language", + "time_zone" + ], "properties": { - "id": { + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" + }, + "language": { + "type": "string", + "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, - "schema.NotificationClearRequest": { + "schema.SiteLegalReq": { "type": "object", + "required": [ + "external_content_display" + ], "properties": { - "type": { - "description": "inbox achievement", + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "privacy_policy_original_text": { + "type": "string" + }, + "privacy_policy_parsed_text": { + "type": "string" + }, + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { "type": "string" } } }, - "schema.PermissionMemberAction": { + "schema.SiteLegalResp": { "type": "object", + "required": [ + "external_content_display" + ], "properties": { - "action": { + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "privacy_policy_original_text": { "type": "string" }, - "name": { + "privacy_policy_parsed_text": { "type": "string" }, - "type": { + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { "type": "string" } } }, - "schema.QuestionAdd": { + "schema.SiteLegalSimpleResp": { "type": "object", "required": [ - "content", - "html", - "tags", - "title" + "external_content_display" ], "properties": { - "content": { - "description": "content", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "html": { - "description": "html", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "tags": { - "description": "tags", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } - }, - "title": { - "description": "question title", + "external_content_display": { "type": "string", - "maxLength": 150, - "minLength": 6 + "enum": [ + "always_display", + "ask_before_display" + ] } } }, - "schema.QuestionSearch": { + "schema.SiteLoginReq": { "type": "object", "properties": { - "order": { - "description": "Search order by", - "type": "string" + "allow_email_domains": { + "type": "array", + "items": { + "type": "string" + } }, - "page": { - "description": "Query number of pages", - "type": "integer" + "allow_email_registrations": { + "type": "boolean" }, - "page_size": { - "description": "Search page size", - "type": "integer" + "allow_new_registrations": { + "type": "boolean" }, - "tags": { - "description": "Search tag", + "allow_password_login": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteLoginResp": { + "type": "object", + "properties": { + "allow_email_domains": { "type": "array", "items": { "type": "string" } }, - "username": { - "description": "Search username", - "type": "string" + "allow_email_registrations": { + "type": "boolean" + }, + "allow_new_registrations": { + "type": "boolean" + }, + "allow_password_login": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" } } }, - "schema.QuestionUpdate": { + "schema.SiteSeoReq": { "type": "object", "required": [ - "content", - "html", - "id", - "tags", - "title" + "permalink", + "robots" ], "properties": { - "content": { - "description": "content", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "edit_summary": { - "description": "edit summary", - "type": "string" - }, - "html": { - "description": "html", - "type": "string", - "maxLength": 65535, - "minLength": 6 + "permalink": { + "type": "integer", + "maximum": 4, + "minimum": 0 }, - "id": { - "description": "question id", + "robots": { "type": "string" - }, - "tags": { - "description": "tags", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } - }, - "title": { - "description": "question title", - "type": "string", - "maxLength": 150, - "minLength": 6 } } }, - "schema.RemoveAnswerReq": { + "schema.SiteSeoResp": { "type": "object", "required": [ - "id" + "permalink", + "robots" ], "properties": { - "id": { - "description": "answer id", + "permalink": { + "type": "integer", + "maximum": 4, + "minimum": 0 + }, + "robots": { "type": "string" } } }, - "schema.RemoveCommentReq": { + "schema.SiteThemeReq": { "type": "object", "required": [ - "comment_id" + "theme" ], "properties": { - "comment_id": { - "description": "comment id", - "type": "string" + "color_scheme": { + "type": "string", + "maxLength": 100 + }, + "theme": { + "type": "string", + "maxLength": 255 + }, + "theme_config": { + "type": "object", + "additionalProperties": true } } }, - "schema.RemoveQuestionReq": { + "schema.SiteThemeResp": { "type": "object", - "required": [ - "id" - ], "properties": { - "id": { - "description": "question id", + "color_scheme": { + "type": "string" + }, + "theme": { "type": "string" + }, + "theme_config": { + "type": "object", + "additionalProperties": true + }, + "theme_options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ThemeOption" + } } } }, - "schema.RemoveTagReq": { + "schema.SiteUsersReq": { "type": "object", "required": [ - "tag_id" + "default_avatar" ], "properties": { - "tag_id": { - "description": "tag_id", + "allow_update_avatar": { + "type": "boolean" + }, + "allow_update_bio": { + "type": "boolean" + }, + "allow_update_display_name": { + "type": "boolean" + }, + "allow_update_location": { + "type": "boolean" + }, + "allow_update_username": { + "type": "boolean" + }, + "allow_update_website": { + "type": "boolean" + }, + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" } } }, - "schema.ReportHandleReq": { + "schema.SiteUsersResp": { "type": "object", "required": [ - "flagged_type", - "id" + "default_avatar" ], "properties": { - "flagged_content": { - "type": "string" + "allow_update_avatar": { + "type": "boolean" }, - "flagged_type": { - "type": "integer" + "allow_update_bio": { + "type": "boolean" }, - "id": { + "allow_update_display_name": { + "type": "boolean" + }, + "allow_update_location": { + "type": "boolean" + }, + "allow_update_username": { + "type": "boolean" + }, + "allow_update_website": { + "type": "boolean" + }, + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" } } }, - "schema.SearchListResp": { + "schema.SiteWriteReq": { "type": "object", "properties": { - "count": { + "authorized_attachment_extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorized_image_extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "max_attachment_size": { + "type": "integer" + }, + "max_image_megapixel": { "type": "integer" }, - "extra": { - "description": "extra fields" + "max_image_size": { + "type": "integer" }, - "list": { - "description": "search response", + "recommend_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.SiteWriteTag" + } + }, + "required_tag": { + "type": "boolean" + }, + "reserved_tags": { "type": "array", "items": { - "$ref": "#/definitions/schema.SearchResp" + "$ref": "#/definitions/schema.SiteWriteTag" } + }, + "restrict_answer": { + "type": "boolean" } } }, - "schema.SearchObject": { + "schema.SiteWriteResp": { "type": "object", "properties": { - "accepted": { - "type": "boolean" + "authorized_attachment_extensions": { + "type": "array", + "items": { + "type": "string" + } }, - "answer_count": { - "type": "integer" + "authorized_image_extensions": { + "type": "array", + "items": { + "type": "string" + } }, - "created_at": { + "max_attachment_size": { "type": "integer" }, - "excerpt": { - "type": "string" - }, - "id": { - "type": "string" + "max_image_megapixel": { + "type": "integer" }, - "status": { - "description": "Status", - "type": "string" + "max_image_size": { + "type": "integer" }, - "tags": { - "description": "tags", + "recommend_tags": { "type": "array", "items": { - "$ref": "#/definitions/schema.TagResp" + "$ref": "#/definitions/schema.SiteWriteTag" } }, - "title": { - "type": "string" + "required_tag": { + "type": "boolean" }, - "user_info": { - "description": "user info", - "$ref": "#/definitions/schema.UserBasicInfo" + "reserved_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.SiteWriteTag" + } }, - "vote_count": { - "type": "integer" + "restrict_answer": { + "type": "boolean" } } }, - "schema.SearchResp": { + "schema.SiteWriteTag": { "type": "object", + "required": [ + "slug_name" + ], "properties": { - "object": { - "description": "this object", - "$ref": "#/definitions/schema.SearchObject" + "display_name": { + "type": "string" }, - "object_type": { - "description": "object_type", + "slug_name": { "type": "string" } } }, - "schema.SiteGeneralReq": { + "schema.TagItem": { "type": "object", - "required": [ - "description", - "name", - "short_description" - ], "properties": { - "description": { + "display_name": { + "description": "display_name", "type": "string", - "maxLength": 2000 + "maxLength": 35 }, - "name": { - "type": "string", - "maxLength": 128 + "original_text": { + "description": "original text", + "type": "string" }, - "short_description": { + "slug_name": { + "description": "slug_name", "type": "string", - "maxLength": 255 + "maxLength": 35 } } }, - "schema.SiteGeneralResp": { + "schema.TagResp": { "type": "object", - "required": [ - "description", - "name", - "short_description" - ], "properties": { - "description": { - "type": "string", - "maxLength": 2000 + "display_name": { + "type": "string" }, - "name": { - "type": "string", - "maxLength": 128 + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" }, - "short_description": { - "type": "string", - "maxLength": 255 + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" } } }, - "schema.SiteInterfaceReq": { + "schema.TagSynonym": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.ThemeOption": { "type": "object", - "required": [ - "language", - "theme" - ], "properties": { - "language": { - "type": "string", - "maxLength": 128 - }, - "logo": { - "type": "string", - "maxLength": 256 + "label": { + "type": "string" }, - "theme": { - "type": "string", - "maxLength": 128 + "value": { + "type": "string" } } }, - "schema.SiteInterfaceResp": { + "schema.UIOptionAction": { "type": "object", - "required": [ - "language", - "theme" - ], "properties": { - "language": { - "type": "string", - "maxLength": 128 + "loading": { + "$ref": "#/definitions/schema.LoadingAction" }, - "logo": { - "type": "string", - "maxLength": 256 + "method": { + "type": "string" }, - "theme": { - "type": "string", - "maxLength": 128 + "on_complete": { + "$ref": "#/definitions/schema.OnCompleteAction" + }, + "url": { + "type": "string" } } }, - "schema.TagItem": { + "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { - "display_name": { - "description": "display_name", - "type": "string", - "maxLength": 35 + "answer_accepted": { + "type": "boolean" }, - "original_text": { - "description": "original text", + "answer_count": { + "type": "integer" + }, + "answer_id": { "type": "string" }, - "parsed_text": { - "description": "parsed text", + "comment_id": { "type": "string" }, - "slug_name": { - "description": "slug_name", - "type": "string", - "maxLength": 35 + "content": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "html": { + "type": "string" + }, + "object_creator_user_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "show_status": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" } } }, - "schema.TagResp": { + "schema.UpdateBadgeStatusReq": { "type": "object", + "required": [ + "id", + "status" + ], "properties": { - "display_name": { - "type": "string" - }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "id": { + "description": "badge id", "type": "string" }, - "slug_name": { - "type": "string" + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] } } }, "schema.UpdateCommentReq": { "type": "object", "required": [ - "comment_id" + "comment_id", + "original_text" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "whether user can delete it", + "type": "string" + }, "comment_id": { "description": "comment id", "type": "string" }, "original_text": { "description": "original comment content", - "type": "string" - }, - "parsed_text": { - "description": "parsed comment content", - "type": "string" + "type": "string", + "maxLength": 600, + "minLength": 2 } } }, @@ -5315,55 +11331,143 @@ const docTemplate = `{ }, "schema.UpdateInfoRequest": { "type": "object", - "required": [ - "display_name" - ], "properties": { "avatar": { - "description": "avatar", - "type": "string", - "maxLength": 500 + "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { - "description": "bio", - "type": "string", - "maxLength": 4096 - }, - "bio_html": { - "description": "bio", "type": "string", "maxLength": 4096 }, "display_name": { - "description": "display_name", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "location": { - "description": "location", "type": "string", "maxLength": 100 }, "username": { - "description": "username", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "website": { - "description": "website", "type": "string", "maxLength": 500 } } }, + "schema.UpdatePluginConfigReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "config_fields": { + "type": "object", + "additionalProperties": {} + }, + "plugin_slug_name": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdatePluginStatusReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "plugin_slug_name": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdatePrivilegesConfigReq": { + "type": "object", + "required": [ + "level" + ], + "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, + "level": { + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + ] + } + } + }, + "schema.UpdateReactionReq": { + "type": "object", + "required": [ + "emoji", + "object_id", + "reaction" + ], + "properties": { + "emoji": { + "type": "string", + "enum": [ + "heart", + "smile", + "frown" + ] + }, + "object_id": { + "type": "string" + }, + "reaction": { + "type": "string", + "enum": [ + "activate", + "deactivate" + ] + } + } + }, + "schema.UpdateReviewReq": { + "type": "object", + "required": [ + "review_id", + "status" + ], + "properties": { + "review_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "approve", + "reject" + ] + } + } + }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { "encryption": { - "description": "\"\" SSL", + "description": "\"\" SSL TLS", "type": "string", "enum": [ - "SSL" + "SSL", + "TLS" ] }, "from_email": { @@ -5418,37 +11522,116 @@ const docTemplate = `{ "description": "original text", "type": "string" }, - "parsed_text": { - "description": "parsed text", + "slug_name": { + "description": "slug_name", + "type": "string", + "maxLength": 35 + }, + "tag_id": { + "description": "tag_id", + "type": "string" + } + } + }, + "schema.UpdateTagSynonymReq": { + "type": "object", + "required": [ + "synonym_tag_list", + "tag_id" + ], + "properties": { + "synonym_tag_list": { + "description": "synonym tag list", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "tag_id": { + "description": "tag_id", + "type": "string" + } + } + }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "color_scheme", + "language" + ], + "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdateUserNotificationConfigReq": { + "type": "object", + "properties": { + "all_new_question": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "all_new_question_for_following_tags": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "inbox": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + }, + "schema.UpdateUserPasswordReq": { + "type": "object", + "required": [ + "password", + "user_id" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "user_id": { "type": "string" + } + } + }, + "schema.UpdateUserPluginConfigReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "config_fields": { + "type": "object", + "additionalProperties": {} }, - "slug_name": { - "description": "slug_name", + "plugin_slug_name": { "type": "string", - "maxLength": 35 - }, - "tag_id": { - "description": "tag_id", - "type": "string" + "maxLength": 100 } } }, - "schema.UpdateTagSynonymReq": { + "schema.UpdateUserRoleReq": { "type": "object", "required": [ - "synonym_tag_list", - "tag_id" + "role_id", + "user_id" ], "properties": { - "synonym_tag_list": { - "description": "synonym tag list", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } + "role_id": { + "description": "role id", + "type": "integer" }, - "tag_id": { - "description": "tag_id", + "user_id": { + "description": "user id", "type": "string" } } @@ -5460,8 +11643,10 @@ const docTemplate = `{ "user_id" ], "properties": { + "remove_all_content": { + "type": "boolean" + }, "status": { - "description": "user status", "type": "string", "enum": [ "normal", @@ -5470,8 +11655,23 @@ const docTemplate = `{ "inactive" ] }, + "suspend_duration": { + "type": "string", + "enum": [ + "24h", + "48h", + "72h", + "7d", + "14d", + "1m", + "2m", + "3m", + "6m", + "1y", + "forever" + ] + }, "user_id": { - "description": "user id", "type": "string" } } @@ -5480,35 +11680,33 @@ const docTemplate = `{ "type": "object", "properties": { "avatar": { - "description": "avatar", "type": "string" }, "display_name": { - "description": "display_name", "type": "string" }, - "ip_info": { - "description": "ip info", + "id": { + "type": "string" + }, + "language": { "type": "string" }, "location": { - "description": "location", "type": "string" }, "rank": { - "description": "rank", "type": "integer" }, "status": { - "description": "status", "type": "string" }, + "suspended_until": { + "type": "integer" + }, "username": { - "description": "name", "type": "string" }, "website": { - "description": "website", "type": "string" } } @@ -5519,9 +11717,20 @@ const docTemplate = `{ "e_mail" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "e_mail": { "type": "string", "maxLength": 500 + }, + "pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 } } }, @@ -5537,53 +11746,212 @@ const docTemplate = `{ } } }, - "schema.UserEmailLogin": { + "schema.UserEmailLoginReq": { "type": "object", + "required": [ + "e_mail", + "pass" + ], "properties": { "captcha_code": { - "description": "captcha_code", "type": "string" }, "captcha_id": { - "description": "captcha_id", "type": "string" }, "e_mail": { - "description": "e_mail", - "type": "string" + "type": "string", + "maxLength": 500 }, "pass": { - "description": "password", + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.UserLoginResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "description": "avatar", + "type": "string" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "have_password": { + "description": "user have password", + "type": "boolean" + }, + "id": { + "description": "user id", + "type": "string" + }, + "language": { + "description": "language", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "visit_token": { + "description": "visit token", + "type": "string" + }, + "website": { + "description": "website", "type": "string" } } }, - "schema.UserModifyPassWordRequest": { + "schema.UserModifyPasswordReq": { "type": "object", + "required": [ + "pass" + ], "properties": { - "old_pass": { - "description": "old password", + "captcha_code": { "type": "string" }, - "pass": { - "description": "password", + "captcha_id": { "type": "string" + }, + "old_pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 } } }, - "schema.UserNoticeSetRequest": { + "schema.UserRankingResp": { "type": "object", "properties": { - "notice_switch": { - "type": "boolean" + "staffs": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_reputation": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_vote": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } } } }, - "schema.UserNoticeSetResp": { + "schema.UserRankingSimpleInfo": { "type": "object", "properties": { - "notice_switch": { - "type": "boolean" + "avatar": { + "description": "avatar", + "type": "string" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "vote", + "type": "integer" } } }, @@ -5595,12 +11963,10 @@ const docTemplate = `{ ], "properties": { "code": { - "description": "code", "type": "string", "maxLength": 100 }, "pass": { - "description": "Password", "type": "string", "maxLength": 32 } @@ -5614,18 +11980,22 @@ const docTemplate = `{ "pass" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "e_mail": { - "description": "email", "type": "string", "maxLength": 500 }, "name": { - "description": "name", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "pass": { - "description": "password", "type": "string", "maxLength": 32, "minLength": 8 @@ -5639,15 +12009,24 @@ const docTemplate = `{ ], "properties": { "captcha_code": { - "description": "captcha_code", "type": "string" }, "captcha_id": { - "description": "captcha_id", "type": "string" }, "e_mail": { - "description": "e_mail", + "type": "string", + "maxLength": 500 + } + } + }, + "schema.UserUnsubscribeNotificationReq": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { "type": "string", "maxLength": 500 } @@ -5659,12 +12038,16 @@ const docTemplate = `{ "object_id" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "is_cancel": { - "description": "is cancel", "type": "boolean" }, "object_id": { - "description": "id", "type": "string" } } @@ -5685,6 +12068,21 @@ const docTemplate = `{ "type": "integer" } } + }, + "translator.LangOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "progress": { + "description": "Translation completion percentage", + "type": "integer" + }, + "value": { + "type": "string" + } + } } }, "securityDefinitions": { @@ -5700,12 +12098,14 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "", Host: "", - BasePath: "", + BasePath: "/", Schemes: []string{}, - Title: "", - Description: "", + Title: "Apache Answer", + Description: "Apache Answer API", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/docs/img/logo.svg b/docs/img/logo.svg index b30be2de7..f07ef0408 100644 --- a/docs/img/logo.svg +++ b/docs/img/logo.svg @@ -1,3 +1,21 @@ + diff --git a/docs/img/screenshot.png b/docs/img/screenshot.png index bffbf63e0..8a823a875 100644 Binary files a/docs/img/screenshot.png and b/docs/img/screenshot.png differ diff --git a/docs/release/LICENSE b/docs/release/LICENSE new file mode 100644 index 000000000..926d64b79 --- /dev/null +++ b/docs/release/LICENSE @@ -0,0 +1,319 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +============================================================================ + APACHE ANSWER SUBCOMPONENTS: + + The Apache answer project contains subcomponents with separate copyright + notices and license terms. Your use of the source code for the these + subcomponents is subject to the terms and conditions of the following + licenses. + +======================================================================== +Apache 2.0 licenses +======================================================================== + +The following components are provided under the Apache 2.0 License. + + (Apache License, Version 2.0) react-helmet-async (https://github.com/staylor/react-helmet-async) [link](./licenses/LICENSE-staylor-react-helmet-async.txt) + (Apache License, Version 2.0) golang-mock (https://github.com/golang/mock) [link](./licenses/LICENSE-golang-mock.txt) + (Apache License, Version 2.0) google-wire (https://github.com/google/wire) [link](./licenses/LICENSE-google-wire.txt) + (Apache License, Version 2.0) mojocn-base64Captcha (https://github.com/mojocn/base64Captcha) [link](./licenses/LICENSE-mojocn-base64Captcha.txt) + (Apache License, Version 2.0) ory-dockertest (https://github.com/ory/dockertest) [link](./licenses/LICENSE-ory-dockertest.txt) + (Apache License, Version 2.0) spf13-cobra (https://github.com/spf13/cobra) [link](./licenses/LICENSE-spf13-cobra.txt) + +======================================================================== +MIT licenses +======================================================================== + +The following components are provided under the MIT License. See project link for details. + + (MIT License) axios (https://github.com/axios/axios) [link](./licenses/LICENSE-axios-axios.txt) + (MIT License) bootstrap (https://github.com/twbs/bootstrap) [link](./licenses/LICENSE-twbs-bootstrap.txt) + (MIT License) icons (https://github.com/twbs/icons) [link](./licenses/LICENSE-twbs-icons.txt) + (MIT License) classnames (https://github.com/JedWatson/classnames) [link](./LICENSE-JedWatson-classnames.txt) + (MIT License) codemirror (https://github.com/codemirror/basic-setup) [link](./licenses/LICENSE-codemirror-basic-setup.txt) + (MIT License) @codemirror/lang-markdown (https://github.com/codemirror/lang-markdown) [link](./licenses/LICENSE-codemirror-lang-markdown.txt) + (MIT License) @codemirror/language-data (https://github.com/codemirror/language-data) [link](./licenses/LICENSE-codemirror-language-data.txt) + (MIT License) @codemirror/state (https://github.com/codemirror/state) [link](./licenses/LICENSE-codemirror-state.txt) + (MIT License) @codemirror/view (https://github.com/codemirror/view) [link](./licenses/LICENSE-codemirror-view.txt) + (MIT License) color (https://github.com/Qix-/color) [link](./licenses/LICENSE-Qix--color.txt) + (MIT License) copy-to-clipboard (https://github.com/sudodoki/copy-to-clipboard) [link](./licenses/LICENSE-sudodoki-copy-to-clipboard.txt) + (MIT License) dayjs (https://github.com/iamkun/dayjs) [link](./licenses/LICENSE-iamkun-dayjs.txt) + (MIT License) i18next (https://github.com/i18next/i18next) [link](./licenses/LICENSE-i18next-i18next.txt) + (MIT License) lodash (https://github.com/lodash/lodash) [link](./licenses/LICENSE-lodash-lodash.txt) + (MIT License) marked (https://github.com/markedjs/marked) [link](./licenses/LICENSE-markedjs-marked.txt) + (MIT License) next-share (https://github.com/Bunlong/next-share) [link](./licenses/LIcENSE-Bunlong-next-share.txt) + (MIT License) node-qrcode (https://github.com/soldair/node-qrcode) [link](./licenses/LICENSE-soldair-qrcode.txt) + (MIT License) react (https://github.com/facebook/react) [link](./licenses/LICENSE-facebook-react.txt) + (MIT License) react-bootstrap (https://github.com/react-bootstrap/react-bootstrap) [link](./licenses/LICENSE-react-bootstrap-react-bootstrap.txt) + (MIT License) react-i18next (https://github.com/i18next/react-i18next) [link](./licenses/LICENSE-i18next-react-i18next.txt) + (MIT License) react-router (https://github.com/remix-run/react-router) [link](./licenses/LICENSE-remix-run-react-router.txt) + (MIT License) swr (https://github.com/vercel/swr) [link](./licenses/LICENSE-vercel-swr.txt) + (MIT License) zustand (https://github.com/pmndrs/zustand) [link](./licenses/LICENSE-pmndrs-zustand.txt) + (MIT License) mozillazg-go-pinyin (https://github.com/mozillazg/go-pinyin) [link](./licenses/LICENSE-mozillazg-go-pinyin.txt) + (MIT License) Machiel-slugify (https://github.com/Machiel/slugify) [link](./licenses/LICENSE-Machiel-slugify.txt) + (MIT License) Masterminds-semver (https://github.com/Masterminds/semver) [link](./licenses/LICENSE-Masterminds-semver.txt) + (MIT License) anargu-gin-brotli (https://github.com/anargu/gin-brotli) [link](./licenses/LICENSE-anargu-gin-brotli.txt) + (MIT License) asaskevich-govalidator (https://github.com/asaskevich/govalidator) [link](./licenses/LICENSE-asaskevich-govalidator.txt) + (MIT License) disintegration-imaging (https://github.com/disintegration/imaging) [link](./licenses/LICENSE-disintegration-imaging.txt) + (MIT License) gin-gonic-gin (https://github.com/gin-gonic/gin) [link](./licenses/LICENSE-gin-gonic-gin.txt) + (MIT License) go-playground-locales (https://github.com/go-playground/locales) [link](./licenses/LICENSE-go-playground-locales.txt) + (MIT License) go-playground-universal-translator (https://github.com/go-playground/universal-translator) [link](./licenses/LICENSE-go-playground-universal-translator.txt) + (MIT License) go-playground-validator (https://github.com/go-playground/validator) [link](./licenses/LICENSE-go-playground-validator.txt) + (MIT License) goccy-go-json (https://github.com/goccy/go-json) [link](./licenses/LICENSE-goccy-go-json.txt) + (MIT License) jinzhu-copier (https://github.com/jinzhu/copier) [link](./licenses/LICENSE-jinzhu-copier.txt) + (MIT License) jinzhu-now (https://github.com/jinzhu/now) [link](./licenses/LICENSE-jinzhu-now.txt) + (MIT License) jordan-wright-email (https://github.com/jordan-wright/email) [link](./licenses/LICENSE-jordan-wright-email.txt) + (MIT License) lib-pq (https://github.com/lib/pq) [link](./licenses/LICENSE-lib-pq.txt) + (MIT License) mattn-go-sqlite3 (https://github.com/mattn/go-sqlite3) [link](./licenses/LICENSE-mattn-go-sqlite3.txt) + (MIT License) segmentfault-pacman (https://github.com/segmentfault/pacman) [link](./licenses/LICENSE-segmentfault-pacman.txt) + (MIT License) robfig-cron (https://github.com/robfig/cron) [link](./licenses/LICENSE-robfig-cron.txt) + (MIT License) scottleedavis-go-exif-remove (https://github.com/scottleedavis/go-exif-remove) [link](./licenses/LICENSE-scottleedavis-go-exif-remove.txt) + (MIT License) stretchr-testify (https://github.com/stretchr/testify) [link](./licenses/LICENSE-stretchr-testify.txt) + (MIT License) swaggo-files (https://github.com/swaggo/files) [link](./licenses/LICENSE-swaggo-files.txt) + (MIT License) swaggo-gin-swagger (https://github.com/swaggo/gin-swagger) [link](./licenses/LICENSE-swaggo-gin-swagger.txt) + (MIT License) swaggo-swag (https://github.com/swaggo/swag) [link](./licenses/LICENSE-swaggo-swag.txt) + (MIT License) tidwall-gjson (https://github.com/tidwall/gjson) [link](./licenses/LICENSE-tidwall-gjson.txt) + (MIT License) yuin-goldmark (https://github.com/yuin/goldmark) [link](./licenses/LICENSE-yuin-goldmark.txt) + (MIT License) go-gomail-gomail (https://gopkg.in/gomail.v2) [link](./licenses/LICENSE-go-gomail-gomail.txt) + (MIT License) front-matter (https://github.com/jxson/front-matter) [link](./licenses/LICENSE-jxson-front-matter.txt) + (MIT License) js-sha256 (https://github.com/emn178/js-sha256) [link](./licenses/LICENSE-emn178-js-sha256.txt) + +======================================================================== +BSD licenses +======================================================================== + +The following components are provided under a BSD license. See project link for details. + + (BSD 2-Clause) bwmarrin-snowflake (https://github.com/bwmarrin/snowflake) [link](./licenses/LICENSE-bwmarrin-snowflake.txt) + (BSD 2-Clause) xorm (https://xorm.io/xorm) [link](./licenses/LICENSE-xorm.txt) + (BSD 3-Clause) google-uuid (https://github.com/google/uuid) [link](./licenses/LICENSE-google-uuid.txt) + (BSD 3-Clause) grokify-html-strip-tags-go (https://github.com/grokify/html-strip-tags-go) [link](./licenses/LICENSE-grokify-html-strip-tags-go.txt) + (BSD 3-Clause) microcosm-cc-bluemonday (https://github.com/microcosm-cc/bluemonday) [link](./licenses/LICENSE-microcosm-cc-bluemonday.txt) + (BSD 3-Clause) cznic-sqlite (https://modernc.org/sqlite) [link](./licenses/LICENSE-cznic-sqlite.txt) + (BSD 3-Clause) jsdiff (https://github.com/kpdecker/jsdiff) [link](./licenses/LICENSE-kpdecker-jsdiff.txt) + (BSD 3-Clause) qs (https://github.com/ljharb/qs) [link](./licenses/LICENSE-ljharb-qs.txt) + +======================================================================== +ISC licenses +======================================================================== + +The following components are provided under a ISC license. See project link for details. + + (ISC License) node-semver (https://github.com/npm/node-semver) [link](./licenses/LICENSE-npm-node-semver.txt) + +======================================================================== +MIT and Apache-2.0 licenses +======================================================================== + +The following components are provided under a MIT and Apache-2.0 license. See project link for details. + + (MIT and Apache-2.0) go-yaml-yaml (https://gopkg.in/yaml.v3) [link](./licenses/LICENSE-go-yaml-yaml.txt) + +======================================================================== +MPL licenses +======================================================================== + +The following components are provided under a MPL license. See project link for details. + + (MPL-2.0) go-sql-driver-mysql (https://github.com/go-sql-driver/mysql) [link](./licenses/LICENSE-go-sql-driver-mysql.txt) diff --git a/docs/release/NOTICE b/docs/release/NOTICE new file mode 100644 index 000000000..1978cd387 --- /dev/null +++ b/docs/release/NOTICE @@ -0,0 +1,5 @@ +Apache Answer +Copyright 2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). diff --git a/docs/release/licenses/LICENSE-JedWatson-classnames.txt b/docs/release/licenses/LICENSE-JedWatson-classnames.txt new file mode 100644 index 000000000..4117bfae0 --- /dev/null +++ b/docs/release/licenses/LICENSE-JedWatson-classnames.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-Machiel-slugify.txt b/docs/release/licenses/LICENSE-Machiel-slugify.txt new file mode 100644 index 000000000..316e77d26 --- /dev/null +++ b/docs/release/licenses/LICENSE-Machiel-slugify.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Machiel Molenaar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/docs/release/licenses/LICENSE-Masterminds-semver.txt b/docs/release/licenses/LICENSE-Masterminds-semver.txt new file mode 100644 index 000000000..9ff7da9c4 --- /dev/null +++ b/docs/release/licenses/LICENSE-Masterminds-semver.txt @@ -0,0 +1,19 @@ +Copyright (C) 2014-2019, Matt Butcher and Matt Farina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-Qix--color.txt b/docs/release/licenses/LICENSE-Qix--color.txt new file mode 100644 index 000000000..45c92a8e7 --- /dev/null +++ b/docs/release/licenses/LICENSE-Qix--color.txt @@ -0,0 +1,20 @@ +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-anargu-gin-brotli.txt b/docs/release/licenses/LICENSE-anargu-gin-brotli.txt new file mode 100644 index 000000000..e9bd20124 --- /dev/null +++ b/docs/release/licenses/LICENSE-anargu-gin-brotli.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Anthony Arostegui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-asaskevich-govalidator.txt b/docs/release/licenses/LICENSE-asaskevich-govalidator.txt new file mode 100644 index 000000000..cacba9102 --- /dev/null +++ b/docs/release/licenses/LICENSE-asaskevich-govalidator.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2020 Alex Saskevich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/release/licenses/LICENSE-axios-axios.txt b/docs/release/licenses/LICENSE-axios-axios.txt new file mode 100644 index 000000000..05006a51e --- /dev/null +++ b/docs/release/licenses/LICENSE-axios-axios.txt @@ -0,0 +1,7 @@ +# Copyright (c) 2014-present Matt Zabriskie & Collaborators + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-bwmarrin-snowflake.txt b/docs/release/licenses/LICENSE-bwmarrin-snowflake.txt new file mode 100644 index 000000000..ef39145e5 --- /dev/null +++ b/docs/release/licenses/LICENSE-bwmarrin-snowflake.txt @@ -0,0 +1,23 @@ +Copyright (c) 2016, Bruce +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-codemirror-basic-setup.txt b/docs/release/licenses/LICENSE-codemirror-basic-setup.txt new file mode 100644 index 000000000..9a91f4861 --- /dev/null +++ b/docs/release/licenses/LICENSE-codemirror-basic-setup.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-codemirror-lang-markdown.txt b/docs/release/licenses/LICENSE-codemirror-lang-markdown.txt new file mode 100644 index 000000000..9a91f4861 --- /dev/null +++ b/docs/release/licenses/LICENSE-codemirror-lang-markdown.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-codemirror-language-data.txt b/docs/release/licenses/LICENSE-codemirror-language-data.txt new file mode 100644 index 000000000..9a91f4861 --- /dev/null +++ b/docs/release/licenses/LICENSE-codemirror-language-data.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-codemirror-state.txt b/docs/release/licenses/LICENSE-codemirror-state.txt new file mode 100644 index 000000000..9a91f4861 --- /dev/null +++ b/docs/release/licenses/LICENSE-codemirror-state.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-codemirror-view.txt b/docs/release/licenses/LICENSE-codemirror-view.txt new file mode 100644 index 000000000..9a91f4861 --- /dev/null +++ b/docs/release/licenses/LICENSE-codemirror-view.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2018-2021 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-cznic-sqlite.txt b/docs/release/licenses/LICENSE-cznic-sqlite.txt new file mode 100644 index 000000000..867b0f379 --- /dev/null +++ b/docs/release/licenses/LICENSE-cznic-sqlite.txt @@ -0,0 +1,26 @@ +Copyright (c) 2017 The Sqlite Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-disintegration-imaging.txt b/docs/release/licenses/LICENSE-disintegration-imaging.txt new file mode 100644 index 000000000..a4144a9d2 --- /dev/null +++ b/docs/release/licenses/LICENSE-disintegration-imaging.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Grigory Dryapak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-emn178-js-sha256.txt b/docs/release/licenses/LICENSE-emn178-js-sha256.txt new file mode 100644 index 000000000..dd24c2363 --- /dev/null +++ b/docs/release/licenses/LICENSE-emn178-js-sha256.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014-2024 Chen, Yi-Cyuan + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-facebook-react.txt b/docs/release/licenses/LICENSE-facebook-react.txt new file mode 100644 index 000000000..b93be9051 --- /dev/null +++ b/docs/release/licenses/LICENSE-facebook-react.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-gin-gonic-gin.txt b/docs/release/licenses/LICENSE-gin-gonic-gin.txt new file mode 100644 index 000000000..1ff7f3706 --- /dev/null +++ b/docs/release/licenses/LICENSE-gin-gonic-gin.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Manuel Martínez-Almeida + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-go-gomail-gomail.txt b/docs/release/licenses/LICENSE-go-gomail-gomail.txt new file mode 100644 index 000000000..5f5c12af7 --- /dev/null +++ b/docs/release/licenses/LICENSE-go-gomail-gomail.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Alexandre Cesaro + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-go-playground-locales.txt b/docs/release/licenses/LICENSE-go-playground-locales.txt new file mode 100644 index 000000000..75854ac4f --- /dev/null +++ b/docs/release/licenses/LICENSE-go-playground-locales.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Go Playground + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/release/licenses/LICENSE-go-playground-universal-translator.txt b/docs/release/licenses/LICENSE-go-playground-universal-translator.txt new file mode 100644 index 000000000..8d8aba15b --- /dev/null +++ b/docs/release/licenses/LICENSE-go-playground-universal-translator.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Go Playground + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-go-playground-validator.txt b/docs/release/licenses/LICENSE-go-playground-validator.txt new file mode 100644 index 000000000..6a2ae9aa4 --- /dev/null +++ b/docs/release/licenses/LICENSE-go-playground-validator.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Dean Karn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/docs/release/licenses/LICENSE-go-sql-driver-mysql.txt b/docs/release/licenses/LICENSE-go-sql-driver-mysql.txt new file mode 100644 index 000000000..14e2f777f --- /dev/null +++ b/docs/release/licenses/LICENSE-go-sql-driver-mysql.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/docs/release/licenses/LICENSE-go-yaml-yaml.txt b/docs/release/licenses/LICENSE-go-yaml-yaml.txt new file mode 100644 index 000000000..2683e4bb1 --- /dev/null +++ b/docs/release/licenses/LICENSE-go-yaml-yaml.txt @@ -0,0 +1,50 @@ + +This project is covered by two different licenses: MIT and Apache. + +#### MIT License #### + +The following files were ported to Go from C files of libyaml, and thus +are still covered by their original MIT license, with the additional +copyright staring in 2011 when the project was ported over: + + apic.go emitterc.go parserc.go readerc.go scannerc.go + writerc.go yamlh.go yamlprivateh.go + +Copyright (c) 2006-2010 Kirill Simonov +Copyright (c) 2006-2011 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +### Apache License ### + +All the remaining project files are covered by the Apache license: + +Copyright (c) 2011-2019 Canonical Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/docs/release/licenses/LICENSE-goccy-go-json.txt b/docs/release/licenses/LICENSE-goccy-go-json.txt new file mode 100644 index 000000000..6449c8bff --- /dev/null +++ b/docs/release/licenses/LICENSE-goccy-go-json.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Masaaki Goshima + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-golang-org-x.txt b/docs/release/licenses/LICENSE-golang-org-x.txt new file mode 100644 index 000000000..ea5ea8986 --- /dev/null +++ b/docs/release/licenses/LICENSE-golang-org-x.txt @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/docs/release/licenses/LICENSE-google-uuid.txt b/docs/release/licenses/LICENSE-google-uuid.txt new file mode 100644 index 000000000..5dc68268d --- /dev/null +++ b/docs/release/licenses/LICENSE-google-uuid.txt @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-google-wire.txt b/docs/release/licenses/LICENSE-google-wire.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/docs/release/licenses/LICENSE-google-wire.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/release/licenses/LICENSE-grokify-html-strip-tags-go.txt b/docs/release/licenses/LICENSE-grokify-html-strip-tags-go.txt new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/docs/release/licenses/LICENSE-grokify-html-strip-tags-go.txt @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-i18next-i18next.txt b/docs/release/licenses/LICENSE-i18next-i18next.txt new file mode 100644 index 000000000..0d900eaad --- /dev/null +++ b/docs/release/licenses/LICENSE-i18next-i18next.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 i18next + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-i18next-react-i18next.txt b/docs/release/licenses/LICENSE-i18next-react-i18next.txt new file mode 100644 index 000000000..0d900eaad --- /dev/null +++ b/docs/release/licenses/LICENSE-i18next-react-i18next.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023 i18next + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-iamkun-dayjs.txt b/docs/release/licenses/LICENSE-iamkun-dayjs.txt new file mode 100644 index 000000000..caf931549 --- /dev/null +++ b/docs/release/licenses/LICENSE-iamkun-dayjs.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-present, iamkun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-jinzhu-copier.txt b/docs/release/licenses/LICENSE-jinzhu-copier.txt new file mode 100644 index 000000000..e2dc5381e --- /dev/null +++ b/docs/release/licenses/LICENSE-jinzhu-copier.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jinzhu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-jinzhu-now.txt b/docs/release/licenses/LICENSE-jinzhu-now.txt new file mode 100644 index 000000000..037e1653e --- /dev/null +++ b/docs/release/licenses/LICENSE-jinzhu-now.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-NOW Jinzhu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-jordan-wright-email.txt b/docs/release/licenses/LICENSE-jordan-wright-email.txt new file mode 100644 index 000000000..678f42d6a --- /dev/null +++ b/docs/release/licenses/LICENSE-jordan-wright-email.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Jordan Wright + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-jxson-front-matter.txt b/docs/release/licenses/LICENSE-jxson-front-matter.txt new file mode 100644 index 000000000..377f5461d --- /dev/null +++ b/docs/release/licenses/LICENSE-jxson-front-matter.txt @@ -0,0 +1,9 @@ +# The MIT License (MIT) + +Copyright (c) Jason Campbell ("Author") + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-kpdecker-jsdiff.txt b/docs/release/licenses/LICENSE-kpdecker-jsdiff.txt new file mode 100644 index 000000000..40099ffec --- /dev/null +++ b/docs/release/licenses/LICENSE-kpdecker-jsdiff.txt @@ -0,0 +1,31 @@ +Software License Agreement (BSD License) + +Copyright (c) 2009-2015, Kevin Decker + +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Kevin Decker nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-lib-pq.txt b/docs/release/licenses/LICENSE-lib-pq.txt new file mode 100644 index 000000000..5773904a3 --- /dev/null +++ b/docs/release/licenses/LICENSE-lib-pq.txt @@ -0,0 +1,8 @@ +Copyright (c) 2011-2013, 'pq' Contributors +Portions Copyright (C) 2011 Blake Mizerany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-ljharb-qs.txt b/docs/release/licenses/LICENSE-ljharb-qs.txt new file mode 100644 index 000000000..fecf6b694 --- /dev/null +++ b/docs/release/licenses/LICENSE-ljharb-qs.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-lodash-lodash.txt b/docs/release/licenses/LICENSE-lodash-lodash.txt new file mode 100644 index 000000000..4773fd8e8 --- /dev/null +++ b/docs/release/licenses/LICENSE-lodash-lodash.txt @@ -0,0 +1,49 @@ +The MIT License + +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/docs/release/licenses/LICENSE-markedjs-marked.txt b/docs/release/licenses/LICENSE-markedjs-marked.txt new file mode 100644 index 000000000..4bd2d4a08 --- /dev/null +++ b/docs/release/licenses/LICENSE-markedjs-marked.txt @@ -0,0 +1,44 @@ +# License information + +## Contribution License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## Marked + +Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Markdown + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. diff --git a/docs/release/licenses/LICENSE-mattn-go-sqlite3.txt b/docs/release/licenses/LICENSE-mattn-go-sqlite3.txt new file mode 100644 index 000000000..ca458bb39 --- /dev/null +++ b/docs/release/licenses/LICENSE-mattn-go-sqlite3.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-microcosm-cc-bluemonday.txt b/docs/release/licenses/LICENSE-microcosm-cc-bluemonday.txt new file mode 100644 index 000000000..2e6c493ba --- /dev/null +++ b/docs/release/licenses/LICENSE-microcosm-cc-bluemonday.txt @@ -0,0 +1,31 @@ +SPDX short identifier: BSD-3-Clause +https://opensource.org/licenses/BSD-3-Clause + +Copyright (c) 2014, David Kitchen + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the organisation (Microcosm) nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-mojocn-base64Captcha.txt b/docs/release/licenses/LICENSE-mojocn-base64Captcha.txt new file mode 100644 index 000000000..373ff54ed --- /dev/null +++ b/docs/release/licenses/LICENSE-mojocn-base64Captcha.txt @@ -0,0 +1,13 @@ +Copyright 2019 Eric neochau@gmail.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/docs/release/licenses/LICENSE-mozillazg-go-pinyin.txt b/docs/release/licenses/LICENSE-mozillazg-go-pinyin.txt new file mode 100644 index 000000000..8a7780fcc --- /dev/null +++ b/docs/release/licenses/LICENSE-mozillazg-go-pinyin.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 mozillazg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-npm-node-semver.txt b/docs/release/licenses/LICENSE-npm-node-semver.txt new file mode 100644 index 000000000..19129e315 --- /dev/null +++ b/docs/release/licenses/LICENSE-npm-node-semver.txt @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/docs/release/licenses/LICENSE-ory-dockertest.txt b/docs/release/licenses/LICENSE-ory-dockertest.txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/docs/release/licenses/LICENSE-ory-dockertest.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/release/licenses/LICENSE-pmndrs-zustand.txt b/docs/release/licenses/LICENSE-pmndrs-zustand.txt new file mode 100644 index 000000000..a2c2649de --- /dev/null +++ b/docs/release/licenses/LICENSE-pmndrs-zustand.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Paul Henschel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-react-bootstrap-react-bootstrap.txt b/docs/release/licenses/LICENSE-react-bootstrap-react-bootstrap.txt new file mode 100644 index 000000000..495b6f119 --- /dev/null +++ b/docs/release/licenses/LICENSE-react-bootstrap-react-bootstrap.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-present Stephen J. Collings, Matthew Honnibal, Pieter Vanderwerff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-remix-run-react-router.txt b/docs/release/licenses/LICENSE-remix-run-react-router.txt new file mode 100644 index 000000000..bb050aea6 --- /dev/null +++ b/docs/release/licenses/LICENSE-remix-run-react-router.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) React Training LLC 2015-2019 Copyright (c) Remix Software Inc. 2020-2021 Copyright (c) Shopify Inc. 2022-2023 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-robfig-cron.txt b/docs/release/licenses/LICENSE-robfig-cron.txt new file mode 100644 index 000000000..3a0f627ff --- /dev/null +++ b/docs/release/licenses/LICENSE-robfig-cron.txt @@ -0,0 +1,21 @@ +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-scottleedavis-go-exif-remove.txt b/docs/release/licenses/LICENSE-scottleedavis-go-exif-remove.txt new file mode 100644 index 000000000..3ba9c83c6 --- /dev/null +++ b/docs/release/licenses/LICENSE-scottleedavis-go-exif-remove.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 scott lee davis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-segmentfault-pacman.txt b/docs/release/licenses/LICENSE-segmentfault-pacman.txt new file mode 100644 index 000000000..ef922b2e8 --- /dev/null +++ b/docs/release/licenses/LICENSE-segmentfault-pacman.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) since 2022 The Segmentfault Development Team. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-soldair-qrcode.txt b/docs/release/licenses/LICENSE-soldair-qrcode.txt new file mode 100644 index 000000000..0a172fc28 --- /dev/null +++ b/docs/release/licenses/LICENSE-soldair-qrcode.txt @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2012 Ryan Day + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-spf13-cobra.txt b/docs/release/licenses/LICENSE-spf13-cobra.txt new file mode 100644 index 000000000..298f0e266 --- /dev/null +++ b/docs/release/licenses/LICENSE-spf13-cobra.txt @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/docs/release/licenses/LICENSE-staylor-react-helmet-async.txt b/docs/release/licenses/LICENSE-staylor-react-helmet-async.txt new file mode 100644 index 000000000..aa7a7d512 --- /dev/null +++ b/docs/release/licenses/LICENSE-staylor-react-helmet-async.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 The New York Times Company + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/release/licenses/LICENSE-stretchr-testify.txt b/docs/release/licenses/LICENSE-stretchr-testify.txt new file mode 100644 index 000000000..4b0421cf9 --- /dev/null +++ b/docs/release/licenses/LICENSE-stretchr-testify.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-sudodoki-copy-to-clipboard.txt b/docs/release/licenses/LICENSE-sudodoki-copy-to-clipboard.txt new file mode 100644 index 000000000..4a7395ee8 --- /dev/null +++ b/docs/release/licenses/LICENSE-sudodoki-copy-to-clipboard.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 sudodoki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-swaggo-files.txt b/docs/release/licenses/LICENSE-swaggo-files.txt new file mode 100644 index 000000000..1667ee95a --- /dev/null +++ b/docs/release/licenses/LICENSE-swaggo-files.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Swaggo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-swaggo-gin-swagger.txt b/docs/release/licenses/LICENSE-swaggo-gin-swagger.txt new file mode 100644 index 000000000..fad18767c --- /dev/null +++ b/docs/release/licenses/LICENSE-swaggo-gin-swagger.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Swaggo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-swaggo-swag.txt b/docs/release/licenses/LICENSE-swaggo-swag.txt new file mode 100644 index 000000000..a97865bf4 --- /dev/null +++ b/docs/release/licenses/LICENSE-swaggo-swag.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Eason Lin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-tidwall-gjson.txt b/docs/release/licenses/LICENSE-tidwall-gjson.txt new file mode 100644 index 000000000..58f5819a4 --- /dev/null +++ b/docs/release/licenses/LICENSE-tidwall-gjson.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-twbs-bootstrap.txt b/docs/release/licenses/LICENSE-twbs-bootstrap.txt new file mode 100644 index 000000000..6633b55fe --- /dev/null +++ b/docs/release/licenses/LICENSE-twbs-bootstrap.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011-2023 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-twbs-icons.txt b/docs/release/licenses/LICENSE-twbs-icons.txt new file mode 100644 index 000000000..3f97be60e --- /dev/null +++ b/docs/release/licenses/LICENSE-twbs-icons.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-2023 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/release/licenses/LICENSE-uber-go-mock.txt b/docs/release/licenses/LICENSE-uber-go-mock.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/docs/release/licenses/LICENSE-uber-go-mock.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/release/licenses/LICENSE-vercel-swr.txt b/docs/release/licenses/LICENSE-vercel-swr.txt new file mode 100644 index 000000000..11f712fa9 --- /dev/null +++ b/docs/release/licenses/LICENSE-vercel-swr.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LICENSE-xorm.txt b/docs/release/licenses/LICENSE-xorm.txt new file mode 100644 index 000000000..84d2ae538 --- /dev/null +++ b/docs/release/licenses/LICENSE-xorm.txt @@ -0,0 +1,27 @@ +Copyright (c) 2013 - 2015 The Xorm Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/release/licenses/LICENSE-yuin-goldmark.txt b/docs/release/licenses/LICENSE-yuin-goldmark.txt new file mode 100644 index 000000000..dc5b2a690 --- /dev/null +++ b/docs/release/licenses/LICENSE-yuin-goldmark.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Yusuke Inuzuka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/release/licenses/LIcENSE-Bunlong-next-share.txt b/docs/release/licenses/LIcENSE-Bunlong-next-share.txt new file mode 100644 index 000000000..860bb4c58 --- /dev/null +++ b/docs/release/licenses/LIcENSE-Bunlong-next-share.txt @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2021 Bunlong + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/swagger.json b/docs/swagger.json index bd08e5547..8cc85e263 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,9 +1,28 @@ { "swagger": "2.0", "info": { + "description": "Apache Answer API", + "title": "Apache Answer", "contact": {} }, + "basePath": "/", "paths": { + "/": { + "get": { + "description": "if config file not exist try to redirect to install page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "if config file not exist try to redirect to install page", + "responses": {} + } + }, "/answer/admin/api/answer/page": { "get": { "security": [ @@ -11,7 +30,7 @@ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "Status:[available,deleted,pending]", "consumes": [ "application/json" ], @@ -21,7 +40,7 @@ "tags": [ "admin" ], - "summary": "CmsSearchList", + "summary": "AdminAnswerPage admin answer page", "parameters": [ { "type": "integer", @@ -38,12 +57,25 @@ { "enum": [ "available", - "deleted" + "deleted", + "pending" ], "type": "string", "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "answer id or question title", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "question id", + "name": "question_id", + "in": "query" } ], "responses": { @@ -63,7 +95,7 @@ "ApiKeyAuth": [] } ], - "description": "Status:[available,deleted]", + "description": "update answer status", "consumes": [ "application/json" ], @@ -73,15 +105,15 @@ "tags": [ "admin" ], - "summary": "AdminSetAnswerStatus", + "summary": "update answer status", "parameters": [ { - "description": "AdminSetAnswerStatusRequest", + "description": "AdminUpdateAnswerStatusReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/entity.AdminSetAnswerStatusRequest" + "$ref": "#/definitions/schema.AdminUpdateAnswerStatusReq" } } ], @@ -95,21 +127,35 @@ } } }, - "/answer/admin/api/language/options": { - "get": { + "/answer/admin/api/badge/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get language options", + "description": "update badge status", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Lang" + "AdminBadge" + ], + "summary": "update badge status", + "parameters": [ + { + "description": "UpdateBadgeStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateBadgeStatusReq" + } + } ], - "summary": "Get language options", "responses": { "200": { "description": "OK", @@ -120,14 +166,14 @@ } } }, - "/answer/admin/api/question/page": { + "/answer/admin/api/badges": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Status:[available,closed,deleted]", + "description": "list all badges by page", "consumes": [ "application/json" ], @@ -135,13 +181,13 @@ "application/json" ], "tags": [ - "admin" + "AdminBadge" ], - "summary": "CmsSearchList", + "summary": "list all badges by page", "parameters": [ { "type": "integer", - "description": "page size", + "description": "page", "name": "page", "in": "query" }, @@ -153,73 +199,55 @@ }, { "enum": [ - "available", - "closed", - "deleted" + "", + "active", + "inactive" ], "type": "string", - "description": "user status", + "description": "badge status", "name": "status", "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, - "/answer/admin/api/question/status": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Status:[available,closed,deleted]", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "AdminSetQuestionStatus", - "parameters": [ + }, { - "description": "AdminSetQuestionStatusRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AdminSetQuestionStatusRequest" - } + "type": "string", + "description": "search param", + "name": "q", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListPagedResp" + } + } + } + } + ] } } } } }, - "/answer/admin/api/reasons": { + "/answer/admin/api/dashboard": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get reasons by object type and action", + "description": "DashboardInfo", "consumes": [ "application/json" ], @@ -227,37 +255,9 @@ "application/json" ], "tags": [ - "reason" - ], - "summary": "get reasons by object type and action", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "enum": [ - "status", - "close", - "flag", - "review" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "admin" ], + "summary": "DashboardInfo", "responses": { "200": { "description": "OK", @@ -268,17 +268,14 @@ } } }, - "/answer/admin/api/report/": { - "put": { + "/answer/admin/api/delete/permanently": { + "delete": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "handle flag", + "description": "delete permanently", "consumes": [ "application/json" ], @@ -288,15 +285,15 @@ "tags": [ "admin" ], - "summary": "handle flag", + "summary": "delete permanently", "parameters": [ { - "description": "flag", + "description": "DeletePermanentlyReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.ReportHandleReq" + "$ref": "#/definitions/schema.DeletePermanentlyReq" } } ], @@ -310,65 +307,21 @@ } } }, - "/answer/admin/api/reports/page": { + "/answer/admin/api/language/options": { "get": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "list report records", - "consumes": [ - "application/json" - ], + "description": "Get language options", "produces": [ "application/json" ], "tags": [ - "admin" - ], - "summary": "list report page", - "parameters": [ - { - "enum": [ - "pending", - "completed" - ], - "type": "string", - "description": "status", - "name": "status", - "in": "query", - "required": true - }, - { - "enum": [ - "all", - "question", - "answer", - "comment" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - } + "Lang" ], + "summary": "Get language options", "responses": { "200": { "description": "OK", @@ -379,21 +332,30 @@ } } }, - "/answer/admin/api/setting/smtp": { + "/answer/admin/api/plugin/config": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetSMTPConfig get smtp config", + "description": "get plugin config", "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" + ], + "summary": "get plugin config", + "parameters": [ + { + "type": "string", + "description": "plugin_slug_name", + "name": "plugin_slug_name", + "in": "query", + "required": true + } ], - "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", @@ -406,7 +368,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetSMTPConfigResp" + "$ref": "#/definitions/schema.GetPluginConfigResp" } } } @@ -421,22 +383,25 @@ "ApiKeyAuth": [] } ], - "description": "update smtp config", + "description": "update plugin config", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "update smtp config", + "summary": "update plugin config", "parameters": [ { - "description": "smtp config", + "description": "UpdatePluginConfigReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateSMTPConfigReq" + "$ref": "#/definitions/schema.UpdatePluginConfigReq" } } ], @@ -450,64 +415,32 @@ } } }, - "/answer/admin/api/siteinfo/general": { - "get": { + "/answer/admin/api/plugin/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", - "produces": [ + "description": "update plugin status", + "consumes": [ "application/json" ], - "tags": [ - "admin" - ], - "summary": "Get siteinfo general", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Get siteinfo interface", "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "Get siteinfo interface", + "summary": "update plugin status", "parameters": [ { - "description": "general", + "description": "UpdatePluginStatusReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.SiteGeneralReq" + "$ref": "#/definitions/schema.UpdatePluginStatusReq" } } ], @@ -521,30 +454,36 @@ } } }, - "/answer/admin/api/siteinfo/interface": { + "/answer/admin/api/plugins": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get plugin list", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "admin" + "AdminPlugin" ], - "summary": "Get siteinfo interface", + "summary": "get plugin list", "parameters": [ { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } + "type": "string", + "description": "status: active/inactive", + "name": "status", + "in": "query" + }, + { + "type": "boolean", + "description": "have config", + "name": "have_config", + "in": "query" } ], "responses": { @@ -559,7 +498,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteInterfaceResp" + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetPluginListResp" + } } } } @@ -567,30 +509,56 @@ } } } - }, - "put": { + } + }, + "/answer/admin/api/question/page": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "Status:[available,closed,deleted,pending]", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "AdminQuestionPage admin question page", "parameters": [ { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.SiteInterfaceReq" - } + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "available", + "closed", + "deleted", + "pending" + ], + "type": "string", + "description": "user status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "question id or title", + "name": "query", + "in": "query" } ], "responses": { @@ -603,21 +571,35 @@ } } }, - "/answer/admin/api/theme/options": { - "get": { + "/answer/admin/api/question/status": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get theme options", + "description": "update question status", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get theme options", + "summary": "update question status", + "parameters": [ + { + "description": "AdminUpdateQuestionStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AdminUpdateQuestionStatusReq" + } + } + ], "responses": { "200": { "description": "OK", @@ -628,14 +610,14 @@ } } }, - "/answer/admin/api/user/status": { - "put": { + "/answer/admin/api/reasons": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update user", + "description": "get reasons by object type and action", "consumes": [ "application/json" ], @@ -643,18 +625,35 @@ "application/json" ], "tags": [ - "admin" + "reason" ], - "summary": "update user", + "summary": "get reasons by object type and action", "parameters": [ { - "description": "user", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserStatusReq" - } + "enum": [ + "question", + "answer", + "comment", + "user" + ], + "type": "string", + "description": "object_type", + "name": "object_type", + "in": "query", + "required": true + }, + { + "enum": [ + "status", + "close", + "flag", + "review" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true } ], "responses": { @@ -667,59 +666,21 @@ } } }, - "/answer/admin/api/users/page": { + "/answer/admin/api/roles": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get user page", + "description": "get role list", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "get user page", - "parameters": [ - { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" - }, - { - "type": "string", - "description": "email", - "name": "e_mail", - "in": "query" - }, - { - "enum": [ - "normal", - "suspended", - "deleted", - "inactive" - ], - "type": "string", - "description": "user status", - "name": "status", - "in": "query" - } - ], + "summary": "get role list", "responses": { "200": { "description": "OK", @@ -732,22 +693,10 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "records": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetUserPageResp" - } - } - } - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRoleResp" + } } } } @@ -757,69 +706,64 @@ } } }, - "/answer/api/v1/answer": { - "put": { + "/answer/admin/api/setting/privileges": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Update Answer", - "consumes": [ - "application/json" - ], + "description": "GetPrivilegesConfig get privileges config", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "Update Answer", - "parameters": [ - { - "description": "AnswerUpdateReq", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AnswerUpdateReq" - } - } + "admin" ], + "summary": "GetPrivilegesConfig get privileges config", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetPrivilegesConfigResp" + } + } + } + ] } } } }, - "post": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Insert Answer", - "consumes": [ - "application/json" - ], + "description": "update privileges config", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "Insert Answer", + "summary": "update privileges config", "parameters": [ { - "description": "AnswerAddReq", + "description": "config", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerAddReq" + "$ref": "#/definitions/schema.UpdatePrivilegesConfigReq" } } ], @@ -827,75 +771,70 @@ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } - }, - "delete": { + } + }, + "/answer/admin/api/setting/smtp": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "delete answer", - "consumes": [ - "application/json" - ], + "description": "GetSMTPConfig get smtp config", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "delete answer", - "parameters": [ - { - "description": "answer", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RemoveAnswerReq" - } - } + "admin" ], + "summary": "GetSMTPConfig get smtp config", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetSMTPConfigResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/answer/acceptance": { - "post": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Adopted", - "consumes": [ - "application/json" - ], + "description": "update smtp config", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "Adopted", + "summary": "update smtp config", "parameters": [ { - "description": "AnswerAdoptedReq", + "description": "smtp config", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerAdoptedReq" + "$ref": "#/definitions/schema.UpdateSMTPConfigReq" } } ], @@ -903,71 +842,70 @@ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/answer/info": { + "/answer/admin/api/siteinfo/branding": { "get": { - "description": "Get Answer", - "consumes": [ - "application/json" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "get site interface", "produces": [ "application/json" ], "tags": [ - "api-answer" - ], - "summary": "Get Answer", - "parameters": [ - { - "type": "string", - "default": "1", - "description": "Answer TagID", - "name": "id", - "in": "query", - "required": true - } + "admin" ], + "summary": "get site interface", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteBrandingResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/answer/list": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", - "consumes": [ - "application/json" - ], + "description": "update site info branding", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "AnswerList", + "summary": "update site info branding", "parameters": [ { - "description": "AnswerList", + "description": "branding info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AnswerList" + "$ref": "#/definitions/schema.SiteBrandingReq" } } ], @@ -975,41 +913,27 @@ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/collection/switch": { - "post": { + "/answer/admin/api/siteinfo/custom-css-html": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add collection", - "consumes": [ - "application/json" - ], + "description": "get site info custom html css config", "produces": [ "application/json" ], "tags": [ - "Collection" - ], - "summary": "add collection", - "parameters": [ - { - "description": "collection", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.CollectionSwitchReq" - } - } + "admin" ], + "summary": "get site info custom html css config", "responses": { "200": { "description": "OK", @@ -1022,7 +946,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.CollectionSwitchResp" + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" } } } @@ -1030,27 +954,57 @@ } } } - } - }, - "/answer/api/v1/comment": { - "get": { - "description": "get comment by id", + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "get comment by id", + "summary": "update site custom css html config", "parameters": [ { - "type": "string", - "description": "id", - "name": "id", - "in": "query", - "required": true + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/siteinfo/general": { + "get": { + "security": [ + { + "ApiKeyAuth": [] } ], + "description": "get site general information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -1063,22 +1017,7 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentResp" - } - } - } - } - ] + "$ref": "#/definitions/schema.SiteGeneralResp" } } } @@ -1093,25 +1032,22 @@ "ApiKeyAuth": [] } ], - "description": "update comment", - "consumes": [ - "application/json" - ], + "description": "update site general information", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "update comment", + "summary": "update site general information", "parameters": [ { - "description": "comment", + "description": "general", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateCommentReq" + "$ref": "#/definitions/schema.SiteGeneralReq" } } ], @@ -1123,35 +1059,23 @@ } } } - }, - "post": { + } + }, + "/answer/admin/api/siteinfo/interface": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add comment", - "consumes": [ - "application/json" - ], + "description": "get site interface", "produces": [ "application/json" ], "tags": [ - "Comment" - ], - "summary": "add comment", - "parameters": [ - { - "description": "comment", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } + "admin" ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -1164,7 +1088,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetCommentResp" + "$ref": "#/definitions/schema.SiteInterfaceResp" } } } @@ -1173,31 +1097,28 @@ } } }, - "delete": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "remove comment", - "consumes": [ - "application/json" - ], + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "remove comment", + "summary": "update site info interface", "parameters": [ { - "description": "comment", + "description": "general", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.RemoveCommentReq" + "$ref": "#/definitions/schema.SiteInterfaceReq" } } ], @@ -1211,46 +1132,21 @@ } } }, - "/answer/api/v1/comment/page": { + "/answer/admin/api/siteinfo/legal": { "get": { - "description": "get comment page", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Set the legal information for the site", "produces": [ "application/json" ], "tags": [ - "Comment" - ], - "summary": "get comment page", - "parameters": [ - { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "object id", - "name": "object_id", - "in": "query", - "required": true - }, - { - "enum": [ - "vote" - ], - "type": "string", - "description": "query condition", - "name": "query_cond", - "in": "query" - } + "admin" ], + "summary": "Set the legal information for the site", "responses": { "200": { "description": "OK", @@ -1263,22 +1159,7 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentResp" - } - } - } - } - ] + "$ref": "#/definitions/schema.SiteLegalResp" } } } @@ -1286,37 +1167,57 @@ } } } - } - }, - "/answer/api/v1/follow": { - "post": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "follow object or cancel follow operation", - "consumes": [ - "application/json" - ], + "description": "update site legal info", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" ], - "summary": "follow object or cancel follow operation", + "summary": "update site legal info", "parameters": [ { - "description": "follow", + "description": "write info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.FollowReq" + "$ref": "#/definitions/schema.SiteLegalReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" } } + } + } + }, + "/answer/admin/api/siteinfo/login": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site info login config", + "produces": [ + "application/json" + ], + "tags": [ + "admin" ], + "summary": "get site info login config", "responses": { "200": { "description": "OK", @@ -1329,7 +1230,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.FollowResp" + "$ref": "#/definitions/schema.SiteLoginResp" } } } @@ -1337,34 +1238,29 @@ } } } - } - }, - "/answer/api/v1/follow/tags": { + }, "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update user follow tags", - "consumes": [ - "application/json" - ], + "description": "update site login", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" ], - "summary": "update user follow tags", + "summary": "update site login", "parameters": [ { - "description": "follow", + "description": "login info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateFollowTagsReq" + "$ref": "#/definitions/schema.SiteLoginReq" } } ], @@ -1378,23 +1274,65 @@ } } }, - "/answer/api/v1/language/config": { + "/answer/admin/api/siteinfo/seo": { "get": { - "description": "get language config mapping", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get site seo information", "produces": [ "application/json" ], "tags": [ - "Lang" + "admin" ], - "summary": "get language config mapping", + "summary": "get site seo information", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteSeoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site seo information", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "update site seo information", "parameters": [ { - "type": "string", - "description": "Accept-Language", - "name": "Accept-Language", - "in": "header", - "required": true + "description": "seo", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteSeoReq" + } } ], "responses": { @@ -1407,72 +1345,65 @@ } } }, - "/answer/api/v1/language/options": { + "/answer/admin/api/siteinfo/theme": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get language options", + "description": "get site info theme config", "produces": [ "application/json" ], "tags": [ - "Lang" + "admin" ], - "summary": "Get language options", + "summary": "get site info theme config", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteThemeResp" + } + } + } + ] } } } - } - }, - "/answer/api/v1/notification/page": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get notification list", - "consumes": [ - "application/json" - ], + "description": "update site custom css html config", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "get notification list", + "summary": "update site custom css html config", "parameters": [ { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "enum": [ - "inbox", - "achievement" - ], - "type": "string", - "description": "type", - "name": "type", - "in": "query", - "required": true + "description": "login info", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SiteThemeReq" + } } ], "responses": { @@ -1485,32 +1416,64 @@ } } }, - "/answer/api/v1/notification/read/state": { - "put": { + "/answer/admin/api/siteinfo/users": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "ClearUnRead", - "consumes": [ + "description": "get site user config", + "produces": [ "application/json" ], + "tags": [ + "admin" + ], + "summary": "get site user config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteUsersResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site info config about users", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "ClearUnRead", + "summary": "update site info config about users", "parameters": [ { - "description": "NotificationClearIDRequest", + "description": "users info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearIDRequest" + "$ref": "#/definitions/schema.SiteUsersReq" } } ], @@ -1524,32 +1487,64 @@ } } }, - "/answer/api/v1/notification/read/state/all": { - "put": { + "/answer/admin/api/siteinfo/write": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "ClearUnRead", - "consumes": [ + "description": "get site interface", + "produces": [ "application/json" ], + "tags": [ + "admin" + ], + "summary": "get site interface", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteWriteResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update site write info", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "ClearUnRead", + "summary": "update site write info", "parameters": [ { - "description": "NotificationClearRequest", + "description": "write info", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearRequest" + "$ref": "#/definitions/schema.SiteWriteReq" } } ], @@ -1563,24 +1558,21 @@ } } }, - "/answer/api/v1/notification/status": { + "/answer/admin/api/theme/options": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetRedDot", - "consumes": [ - "application/json" - ], + "description": "Get theme options", "produces": [ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "GetRedDot", + "summary": "Get theme options", "responses": { "200": { "description": "OK", @@ -1589,14 +1581,16 @@ } } } - }, - "put": { + } + }, + "/answer/admin/api/user": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "DelRedDot", + "description": "add user", "consumes": [ "application/json" ], @@ -1604,17 +1598,17 @@ "application/json" ], "tags": [ - "Notification" + "admin" ], - "summary": "DelRedDot", + "summary": "add user", "parameters": [ { - "description": "NotificationClearRequest", + "description": "user", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.NotificationClearRequest" + "$ref": "#/definitions/schema.AddUserReq" } } ], @@ -1628,57 +1622,26 @@ } } }, - "/answer/api/v1/personal/answer/page": { + "/answer/admin/api/user/activation": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserAnswerList", - "consumes": [ - "application/json" - ], + "description": "get user activation", "produces": [ "application/json" ], "tags": [ - "api-answer" + "admin" ], - "summary": "UserAnswerList", + "summary": "get user activation", "parameters": [ { "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "score" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "0", - "description": "page", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", + "description": "user id", + "name": "user_id", "in": "query", "required": true } @@ -1687,20 +1650,32 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserActivationResp" + } + } + } + ] } } } } }, - "/answer/api/v1/personal/collection/page": { - "get": { + "/answer/admin/api/user/password": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserCollectionList", + "description": "update user password", "consumes": [ "application/json" ], @@ -1708,25 +1683,18 @@ "application/json" ], "tags": [ - "Collection" + "admin" ], - "summary": "UserCollectionList", + "summary": "update user password", "parameters": [ { - "type": "string", - "default": "0", - "description": "page", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPasswordReq" + } } ], "responses": { @@ -1739,81 +1707,53 @@ } } }, - "/answer/api/v1/personal/comment/page": { - "get": { - "description": "user personal comment list", + "/answer/admin/api/user/profile": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "edit user profile", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Comment" + "admin" ], - "summary": "user personal comment list", + "summary": "edit user profile", "parameters": [ { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.EditUserProfileReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/qa/top": { - "get": { + "/answer/admin/api/user/role": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserTop", + "description": "update user role", "consumes": [ "application/json" ], @@ -1821,17 +1761,18 @@ "application/json" ], "tags": [ - "api-question" + "admin" ], - "summary": "UserTop", + "summary": "update user role", "parameters": [ { - "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserRoleReq" + } } ], "responses": { @@ -1844,81 +1785,53 @@ } } }, - "/answer/api/v1/personal/rank/page": { - "get": { - "description": "user personal rank list", + "/answer/admin/api/user/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Rank" + "admin" ], - "summary": "user personal rank list", + "summary": "update user", "parameters": [ { - "type": "integer", - "description": "page", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "username", - "name": "username", - "in": "query" + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserStatusReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRankPersonalWithPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/user/info": { - "get": { + "/answer/admin/api/users": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "GetOtherUserInfoByUsername", + "description": "add users", "consumes": [ "application/json" ], @@ -1926,58 +1839,81 @@ "application/json" ], "tags": [ - "User" + "admin" ], - "summary": "GetOtherUserInfoByUsername", + "summary": "add users", "parameters": [ { - "type": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true + "description": "user", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddUsersReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetOtherUserInfoResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/personal/vote/page": { - "get": { + "/answer/admin/api/users/activation": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "user's vote", - "consumes": [ - "application/json" - ], + "description": "send user activation", "produces": [ "application/json" ], "tags": [ - "Activity" + "admin" + ], + "summary": "send user activation", + "parameters": [ + { + "description": "SendUserActivationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.SendUserActivationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/users/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user page", + "produces": [ + "application/json" + ], + "tags": [ + "admin" ], - "summary": "user's votes", + "summary": "get user page", "parameters": [ { "type": "integer", @@ -1990,6 +1926,29 @@ "description": "page size", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "search query: email, username or id:[id]", + "name": "query", + "in": "query" + }, + { + "type": "boolean", + "description": "staff user", + "name": "staff", + "in": "query" + }, + { + "enum": [ + "suspended", + "deleted", + "inactive" + ], + "type": "string", + "description": "user status", + "name": "status", + "in": "query" } ], "responses": { @@ -2011,10 +1970,10 @@ { "type": "object", "properties": { - "list": { + "records": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetVoteWithPageResp" + "$ref": "#/definitions/schema.GetUserPageResp" } } } @@ -2029,14 +1988,118 @@ } } }, - "/answer/api/v1/question": { + "/answer/api/v1/activity/timeline": { + "get": { + "description": "get object timeline", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query" + }, + { + "type": "string", + "description": "tag slug name", + "name": "tag_slug_name", + "in": "query" + }, + { + "enum": [ + "question", + "answer", + "tag" + ], + "type": "string", + "description": "object type", + "name": "object_type", + "in": "query" + }, + { + "type": "boolean", + "description": "is show vote", + "name": "show_vote", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/activity/timeline/detail": { + "get": { + "description": "get object timeline detail", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "get object timeline detail", + "parameters": [ + { + "type": "string", + "description": "revision id", + "name": "revision_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetObjectTimelineResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/answer": { "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "update question", + "description": "Update Answer", "consumes": [ "application/json" ], @@ -2044,17 +2107,17 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "update question", + "summary": "Update Answer", "parameters": [ { - "description": "question", + "description": "AnswerUpdateReq", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionUpdate" + "$ref": "#/definitions/schema.AnswerUpdateReq" } } ], @@ -2073,7 +2136,7 @@ "ApiKeyAuth": [] } ], - "description": "add question", + "description": "add answer", "consumes": [ "application/json" ], @@ -2081,17 +2144,17 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "add question", + "summary": "Add Answer", "parameters": [ { - "description": "question", + "description": "add answer request", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionAdd" + "$ref": "#/definitions/schema.AnswerAddReq" } } ], @@ -2110,7 +2173,7 @@ "ApiKeyAuth": [] } ], - "description": "delete question", + "description": "delete answer", "consumes": [ "application/json" ], @@ -2118,17 +2181,17 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "delete question", + "summary": "delete answer", "parameters": [ { - "description": "question", + "description": "answer", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.RemoveQuestionReq" + "$ref": "#/definitions/schema.RemoveAnswerReq" } } ], @@ -2142,14 +2205,14 @@ } } }, - "/answer/api/v1/question/closemsglist": { - "get": { + "/answer/api/v1/answer/acceptance": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "close question msg list", + "description": "Accept Answer", "consumes": [ "application/json" ], @@ -2157,9 +2220,20 @@ "application/json" ], "tags": [ - "api-question" + "Answer" + ], + "summary": "Accept Answer", + "parameters": [ + { + "description": "AcceptAnswerReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AcceptAnswerReq" + } + } ], - "summary": "close question msg list", "responses": { "200": { "description": "OK", @@ -2170,14 +2244,9 @@ } } }, - "/answer/api/v1/question/info": { + "/answer/api/v1/answer/info": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "GetQuestion Question", + "description": "Get Answer Detail", "consumes": [ "application/json" ], @@ -2185,14 +2254,13 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "GetQuestion Question", + "summary": "Get Answer Detail", "parameters": [ { "type": "string", - "default": "1", - "description": "Question TagID", + "description": "id", "name": "id", "in": "query", "required": true @@ -2202,15 +2270,27 @@ "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetAnswerInfoResp" + } + } + } + ] } } } } }, - "/answer/api/v1/question/page": { + "/answer/api/v1/answer/page": { "get": { - "description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)", + "description": "AnswerList \u003cbr\u003e \u003cb\u003eorder\u003c/b\u003e (default or updated)", "consumes": [ "application/json" ], @@ -2218,18 +2298,37 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "SearchQuestionList", + "summary": "AnswerList", "parameters": [ { - "description": "QuestionSearch", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.QuestionSearch" - } + "type": "string", + "description": "question_id", + "name": "question_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true } ], "responses": { @@ -2242,9 +2341,14 @@ } } }, - "/answer/api/v1/question/search": { + "/answer/api/v1/answer/recover": { "post": { - "description": "SearchQuestionList", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover the deleted answer", "consumes": [ "application/json" ], @@ -2252,17 +2356,17 @@ "application/json" ], "tags": [ - "api-question" + "Answer" ], - "summary": "SearchQuestionList", + "summary": "recover answer", "parameters": [ { - "description": "QuestionSearch", + "description": "answer", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.QuestionSearch" + "$ref": "#/definitions/schema.RecoverAnswerReq" } } ], @@ -2270,20 +2374,15 @@ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/question/similar": { + "/answer/api/v1/badge": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "add question title like", + "description": "get badge info", "consumes": [ "application/json" ], @@ -2291,15 +2390,15 @@ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "add question title like", + "summary": "get badge info", "parameters": [ { "type": "string", "default": "string", - "description": "title", - "name": "title", + "description": "id", + "name": "id", "in": "query", "required": true } @@ -2308,15 +2407,27 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } + } } } }, - "/answer/api/v1/question/similar/tag": { + "/answer/api/v1/badge/awards/page": { "get": { - "description": "Search Similar Question", + "description": "get badge award list", "consumes": [ "application/json" ], @@ -2324,37 +2435,61 @@ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "Search Similar Question", + "summary": "get badge award list", "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, { "type": "string", - "default": "", - "description": "question_id", - "name": "question_id", + "description": "badge id", + "name": "badge_id", "in": "query", "required": true + }, + { + "type": "string", + "description": "only list the award by username", + "name": "username", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] } } } } }, - "/answer/api/v1/question/status": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Close question", + "/answer/api/v1/badge/user/awards": { + "get": { + "description": "get user badge award list", "consumes": [ "application/json" ], @@ -2362,51 +2497,63 @@ "application/json" ], "tags": [ - "api-question" + "api-badge" ], - "summary": "Close question", + "summary": "get user badge award list", "parameters": [ { - "description": "question", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.CloseQuestionReq" - } + "type": "string", + "description": "user name", + "name": "username", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] } } } } }, - "/answer/api/v1/question/tags": { + "/answer/api/v1/badge/user/awards/recent": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } + "description": "get user badge award list", + "consumes": [ + "application/json" ], - "description": "get tag list", "produces": [ "application/json" ], "tags": [ - "Tag" + "api-badge" ], - "summary": "get tag list", + "summary": "get user badge award list", "parameters": [ { "type": "string", - "description": "tag", - "name": "tag", - "in": "query" + "description": "user name", + "name": "username", + "in": "query", + "required": true } ], "responses": { @@ -2423,7 +2570,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" } } } @@ -2434,14 +2581,9 @@ } } }, - "/answer/api/v1/reasons": { + "/answer/api/v1/badges": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "get reasons by object type and action", + "description": "list all badges group by group", "consumes": [ "application/json" ], @@ -2449,58 +2591,42 @@ "application/json" ], "tags": [ - "reason" - ], - "summary": "get reasons by object type and action", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "object_type", - "name": "object_type", - "in": "query", - "required": true - }, - { - "enum": [ - "status", - "close", - "flag", - "review" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "api-badge" ], + "summary": "list all badges group by group", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListResp" + } + } + } + } + ] } } } } }, - "/answer/api/v1/report": { + "/answer/api/v1/collection/switch": { "post": { "security": [ - { - "ApiKeyAuth": [] - }, { "ApiKeyAuth": [] } ], - "description": "add report \u003cbr\u003e source (question, answer, comment, user)", + "description": "add collection", "consumes": [ "application/json" ], @@ -2508,54 +2634,19 @@ "application/json" ], "tags": [ - "Report" + "Collection" ], - "summary": "add report", + "summary": "add collection", "parameters": [ { - "description": "report", + "description": "collection", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.AddReportReq" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" + "$ref": "#/definitions/schema.CollectionSwitchReq" } } - } - } - }, - "/answer/api/v1/report/type/list": { - "get": { - "description": "get report type list", - "produces": [ - "application/json" - ], - "tags": [ - "Report" - ], - "summary": "get report type list", - "parameters": [ - { - "enum": [ - "question", - "answer", - "comment", - "user" - ], - "type": "string", - "description": "report source", - "name": "source", - "in": "query", - "required": true - } ], "responses": { "200": { @@ -2569,10 +2660,7 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetReportTypeResp" - } + "$ref": "#/definitions/schema.CollectionSwitchResp" } } } @@ -2582,21 +2670,21 @@ } } }, - "/answer/api/v1/revisions": { + "/answer/api/v1/comment": { "get": { - "description": "get revision list", + "description": "get comment by id", "produces": [ "application/json" ], "tags": [ - "Revision" + "Comment" ], - "summary": "get revision list", + "summary": "get comment by id", "parameters": [ { "type": "string", - "description": "object id", - "name": "object_id", + "description": "id", + "name": "id", "in": "query", "required": true } @@ -2613,10 +2701,22 @@ "type": "object", "properties": { "data": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetRevisionResp" - } + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentResp" + } + } + } + } + ] } } } @@ -2624,102 +2724,51 @@ } } } - } - }, - "/answer/api/v1/search": { - "get": { + }, + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "search object", + "description": "update comment", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Search" + "Comment" ], - "summary": "search object", + "summary": "update comment", "parameters": [ { - "type": "string", - "description": "query string", - "name": "q", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "active", - "score", - "relevance" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", + "description": "comment", + "name": "data", + "in": "body", + "required": true, "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SearchListResp" - } - } - } - ] + "$ref": "#/definitions/schema.UpdateCommentReq" } } - } - } - }, - "/answer/api/v1/siteinfo": { - "get": { - "description": "Get siteinfo", - "produces": [ - "application/json" ], - "tags": [ - "site" - ], - "summary": "Get siteinfo", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } - } - }, - "/answer/api/v1/tag": { - "get": { - "description": "get tag one", + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add comment", "consumes": [ "application/json" ], @@ -2727,23 +2776,18 @@ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "get tag one", + "summary": "add comment", "parameters": [ { - "type": "string", - "description": "tag id", - "name": "tag_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "tag name", - "name": "tag_name", - "in": "query", - "required": true + "description": "comment", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddCommentReq" + } } ], "responses": { @@ -2758,7 +2802,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetCommentResp" } } } @@ -2767,8 +2811,13 @@ } } }, - "put": { - "description": "update tag", + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "remove comment", "consumes": [ "application/json" ], @@ -2776,17 +2825,17 @@ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "update tag", + "summary": "remove comment", "parameters": [ { - "description": "tag", + "description": "comment", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateTagReq" + "$ref": "#/definitions/schema.RemoveCommentReq" } } ], @@ -2798,43 +2847,88 @@ } } } - }, - "delete": { - "description": "delete tag", - "consumes": [ - "application/json" - ], + } + }, + "/answer/api/v1/comment/page": { + "get": { + "description": "get comment page", "produces": [ "application/json" ], "tags": [ - "Tag" + "Comment" ], - "summary": "delete tag", + "summary": "get comment page", "parameters": [ { - "description": "tag", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.RemoveTagReq" - } + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + }, + { + "enum": [ + "vote" + ], + "type": "string", + "description": "query condition", + "name": "query_cond", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentResp" + } + } + } + } + ] + } + } + } + ] } } } } }, - "/answer/api/v1/tag/synonym": { - "put": { - "description": "update tag", + "/answer/api/v1/connector/binding/email": { + "post": { + "description": "external login binding user send email", "consumes": [ "application/json" ], @@ -2842,17 +2936,17 @@ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "update tag", + "summary": "external login binding user send email", "parameters": [ { - "description": "tag", + "description": "external login binding user send email", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateTagSynonymReq" + "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq" } } ], @@ -2860,31 +2954,39 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp" + } + } + } + ] } } } } }, - "/answer/api/v1/tag/synonyms": { + "/answer/api/v1/connector/info": { "get": { - "description": "get tag synonyms", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get all enabled connectors", "produces": [ "application/json" ], "tags": [ - "Tag" - ], - "summary": "get tag synonyms", - "parameters": [ - { - "type": "integer", - "description": "tag id", - "name": "tag_id", - "in": "query", - "required": true - } + "PluginConnector" ], + "summary": "get all enabled connectors", "responses": { "200": { "description": "OK", @@ -2899,7 +3001,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagSynonymsResp" + "$ref": "#/definitions/schema.ConnectorInfoResp" } } } @@ -2910,21 +3012,21 @@ } } }, - "/answer/api/v1/tags/following": { + "/answer/api/v1/connector/user/info": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get following tag list", + "description": "get all connectors info about user", "produces": [ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "get following tag list", + "summary": "get all connectors info about user", "responses": { "200": { "description": "OK", @@ -2939,7 +3041,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetFollowingTagsResp" + "$ref": "#/definitions/schema.ConnectorUserInfoResp" } } } @@ -2950,110 +3052,58 @@ } } }, - "/answer/api/v1/tags/page": { - "get": { - "description": "get tag page", + "/answer/api/v1/connector/user/unbinding": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "unbind external user login", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "Tag" + "PluginConnector" ], - "summary": "get tag page", + "summary": "unbind external user login", "parameters": [ { - "type": "integer", - "description": "page size", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "page size", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "slug_name", - "name": "slug_name", - "in": "query" - }, - { - "enum": [ - "popular", - "name", - "newest" - ], - "type": "string", - "description": "query condition", - "name": "query_cond", - "in": "query" + "description": "ExternalLoginUnbindingReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ExternalLoginUnbindingReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "allOf": [ - { - "$ref": "#/definitions/pager.PageModel" - }, - { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/definitions/schema.GetTagPageResp" - } - } - } - } - ] - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/action/record": { + "/answer/api/v1/embed/config": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } + "description": "get embed plugin config", + "consumes": [ + "application/json" ], - "description": "ActionRecord", - "tags": [ - "User" + "produces": [ + "application/json" ], - "summary": "ActionRecord", - "parameters": [ - { - "enum": [ - "login", - "e_mail", - "find_pass" - ], - "type": "string", - "description": "action", - "name": "action", - "in": "query", - "required": true - } + "tags": [ + "Plugin" ], + "summary": "get embed plugin config", "responses": { "200": { "description": "OK", @@ -3066,7 +3116,10 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.ActionRecordResp" + "type": "array", + "items": { + "$ref": "#/definitions/plugin.EmbedConfig" + } } } } @@ -3076,22 +3129,35 @@ } } }, - "/answer/api/v1/user/avatar/upload": { + "/answer/api/v1/file": { "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserUpdateInfo", + "description": "upload file", "consumes": [ "multipart/form-data" ], "tags": [ - "User" + "Upload" ], - "summary": "UserUpdateInfo", + "summary": "upload file", "parameters": [ + { + "enum": [ + "post", + "post_attachment", + "avatar", + "branding" + ], + "type": "string", + "description": "identify the source of the file upload", + "name": "source", + "in": "formData", + "required": true + }, { "type": "file", "description": "file", @@ -3122,14 +3188,14 @@ } } }, - "/answer/api/v1/user/email": { - "put": { + "/answer/api/v1/follow": { + "post": { "security": [ { "ApiKeyAuth": [] } ], - "description": "user change email verification", + "description": "follow object or cancel follow operation", "consumes": [ "application/json" ], @@ -3137,17 +3203,17 @@ "application/json" ], "tags": [ - "User" + "Activity" ], - "summary": "user change email verification", + "summary": "follow object or cancel follow operation", "parameters": [ { - "description": "UserChangeEmailVerifyReq", + "description": "follow", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" + "$ref": "#/definitions/schema.FollowReq" } } ], @@ -3155,15 +3221,32 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.FollowResp" + } + } + } + ] } } } } }, - "/answer/api/v1/user/email/change/code": { - "post": { - "description": "send email to the user email then change their email", + "/answer/api/v1/follow/tags": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user follow tags", "consumes": [ "application/json" ], @@ -3171,17 +3254,17 @@ "application/json" ], "tags": [ - "User" + "Activity" ], - "summary": "send email to the user email then change their email", + "summary": "update user follow tags", "parameters": [ { - "description": "UserChangeEmailSendCodeReq", + "description": "follow", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" + "$ref": "#/definitions/schema.UpdateFollowTagsReq" } } ], @@ -3195,26 +3278,22 @@ } } }, - "/answer/api/v1/user/email/verification": { - "post": { - "description": "UserVerifyEmail", - "consumes": [ - "application/json" - ], + "/answer/api/v1/language/config": { + "get": { + "description": "get language config mapping", "produces": [ "application/json" ], "tags": [ - "User" + "Lang" ], - "summary": "UserVerifyEmail", + "summary": "get language config mapping", "parameters": [ { "type": "string", - "default": "", - "description": "code", - "name": "code", - "in": "query", + "description": "Accept-Language", + "name": "Accept-Language", + "in": "header", "required": true } ], @@ -3222,76 +3301,35 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/email/verification/send": { - "post": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserVerifyEmailSend", - "consumes": [ - "application/json" - ], + "/answer/api/v1/language/options": { + "get": { + "description": "Get language options", "produces": [ "application/json" ], "tags": [ - "User" - ], - "summary": "UserVerifyEmailSend", - "parameters": [ - { - "type": "string", - "default": "", - "description": "captcha_id", - "name": "captcha_id", - "in": "query" - }, - { - "type": "string", - "default": "", - "description": "captcha_code", - "name": "captcha_code", - "in": "query" - } + "Lang" ], + "summary": "Get language options", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/info": { + "/answer/api/v1/meta/reaction": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "GetUserInfoByUserID", + "description": "get reaction for an object", "consumes": [ "application/json" ], @@ -3299,9 +3337,18 @@ "application/json" ], "tags": [ - "User" + "Meta" + ], + "summary": "get reaction", + "parameters": [ + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query", + "required": true + } ], - "summary": "GetUserInfoByUserID", "responses": { "200": { "description": "OK", @@ -3314,7 +3361,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetUserResp" + "$ref": "#/definitions/schema.ReactionRespItem" } } } @@ -3329,7 +3376,7 @@ "ApiKeyAuth": [] } ], - "description": "UserUpdateInfo update user info", + "description": "update reaction. if not exist, add one", "consumes": [ "application/json" ], @@ -3337,24 +3384,17 @@ "application/json" ], "tags": [ - "User" + "Meta" ], - "summary": "UserUpdateInfo update user info", + "summary": "add or update reaction", "parameters": [ { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", + "description": "reaction", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UpdateInfoRequest" + "$ref": "#/definitions/schema.UpdateReactionReq" } } ], @@ -3368,9 +3408,14 @@ } } }, - "/answer/api/v1/user/login/email": { - "post": { - "description": "UserEmailLogin", + "/answer/api/v1/notification/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get notification list", "consumes": [ "application/json" ], @@ -3378,55 +3423,47 @@ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserEmailLogin", + "summary": "get notification list", "parameters": [ { - "description": "UserEmailLogin", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserEmailLogin" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] - } + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "inbox", + "achievement" + ], + "type": "string", + "description": "type", + "name": "type", + "in": "query", + "required": true + }, + { + "enum": [ + "all", + "posts", + "invites", + "votes" + ], + "type": "string", + "description": "inbox_type", + "name": "inbox_type", + "in": "query", + "required": true } - } - } - }, - "/answer/api/v1/user/logout": { - "get": { - "description": "user logout", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" ], - "tags": [ - "User" - ], - "summary": "user logout", "responses": { "200": { "description": "OK", @@ -3437,14 +3474,14 @@ } } }, - "/answer/api/v1/user/notice/set": { - "post": { + "/answer/api/v1/notification/read/state": { + "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserNoticeSet", + "description": "ClearUnRead", "consumes": [ "application/json" ], @@ -3452,17 +3489,17 @@ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserNoticeSet", + "summary": "ClearUnRead", "parameters": [ { - "description": "UserNoticeSetRequest", + "description": "NotificationClearIDRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserNoticeSetRequest" + "$ref": "#/definitions/schema.NotificationClearIDRequest" } } ], @@ -3470,32 +3507,20 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.UserNoticeSetResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/password": { + "/answer/api/v1/notification/read/state/all": { "put": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserModifyPassWord", + "description": "ClearUnRead", "consumes": [ "application/json" ], @@ -3503,17 +3528,17 @@ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "UserModifyPassWord", + "summary": "ClearUnRead", "parameters": [ { - "description": "UserModifyPassWordRequest", + "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserModifyPassWordRequest" + "$ref": "#/definitions/schema.NotificationClearRequest" } } ], @@ -3527,9 +3552,14 @@ } } }, - "/answer/api/v1/user/password/replacement": { - "post": { - "description": "UseRePassWord", + "/answer/api/v1/notification/status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "GetRedDot", "consumes": [ "application/json" ], @@ -3537,33 +3567,25 @@ "application/json" ], "tags": [ - "User" - ], - "summary": "UseRePassWord", - "parameters": [ - { - "description": "UserRePassWordRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserRePassWordRequest" - } - } + "Notification" ], + "summary": "GetRedDot", "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } - } - }, - "/answer/api/v1/user/password/reset": { - "post": { - "description": "RetrievePassWord", + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DelRedDot", "consumes": [ "application/json" ], @@ -3571,17 +3593,17 @@ "application/json" ], "tags": [ - "User" + "Notification" ], - "summary": "RetrievePassWord", + "summary": "DelRedDot", "parameters": [ { - "description": "UserRetrievePassWordRequest", + "description": "NotificationClearRequest", "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/schema.UserRetrievePassWordRequest" + "$ref": "#/definitions/schema.NotificationClearRequest" } } ], @@ -3589,33 +3611,80 @@ "200": { "description": "OK", "schema": { - "type": "string" + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/post/file": { - "post": { + "/answer/api/v1/permission": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "upload user post file", - "consumes": [ - "multipart/form-data" + "description": "check user permission", + "produces": [ + "application/json" ], "tags": [ - "User" + "Permission" ], - "summary": "upload user post file", + "summary": "check user permission", "parameters": [ { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "enum": [ + "question.add", + "question.edit", + "question.edit_without_review", + "question.delete", + "question.close", + "question.reopen", + "question.vote_up", + "question.vote_down", + "question.pin", + "question.unpin", + "question.hide", + "question.show", + "answer.add", + "answer.edit", + "answer.edit_without_review", + "answer.delete", + "answer.accept", + "answer.vote_up", + "answer.vote_down", + "answer.invite_someone_to_answer", + "comment.add", + "comment.edit", + "comment.delete", + "comment.vote_up", + "comment.vote_down", + "report.add", + "tag.add", + "tag.edit", + "tag.edit_slug_name", + "tag.edit_without_review", + "tag.delete", + "tag.synonym", + "link.url_limit", + "vote.detail", + "answer.audit", + "question.audit", + "tag.audit", + "tag.use_reserved_tag" + ], + "type": "string", + "description": "permission key", + "name": "action", + "in": "query", "required": true } ], @@ -3631,7 +3700,10 @@ "type": "object", "properties": { "data": { - "type": "string" + "type": "object", + "additionalProperties": { + "type": "boolean" + } } } } @@ -3641,9 +3713,14 @@ } } }, - "/answer/api/v1/user/register/email": { - "post": { - "description": "UserRegisterByEmail", + "/answer/api/v1/personal/answer/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list personal answers", "consumes": [ "application/json" ], @@ -3651,50 +3728,64 @@ "application/json" ], "tags": [ - "User" + "Personal" ], - "summary": "UserRegisterByEmail", + "summary": "list personal answers", "parameters": [ { - "description": "UserRegisterReq", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UserRegisterReq" - } + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "score" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/user/status": { + "/answer/api/v1/personal/collection/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "get user status info", + "description": "list personal collections", "consumes": [ "application/json" ], @@ -3702,39 +3793,107 @@ "application/json" ], "tags": [ - "User" + "Collection" ], - "summary": "get user status info", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/handler.RespBody" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/schema.GetUserResp" - } - } - } - ] + "summary": "list personal collections", + "parameters": [ + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" } } } } }, - "/answer/api/v1/vote/down": { - "post": { - "security": [ + "/answer/api/v1/personal/comment/page": { + "get": { + "description": "user personal comment list", + "produces": [ + "application/json" + ], + "tags": [ + "Comment" + ], + "summary": "user personal comment list", + "parameters": [ { - "ApiKeyAuth": [] + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "username", + "name": "username", + "in": "query" } ], - "description": "add vote", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetCommentPersonalWithPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/personal/qa/top": { + "get": { + "description": "UserTop", "consumes": [ "application/json" ], @@ -3742,19 +3901,58 @@ "application/json" ], "tags": [ - "Activity" + "Question" ], - "summary": "vote down", + "summary": "UserTop", "parameters": [ { - "description": "vote", - "name": "data", - "in": "body", - "required": true, + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/schema.VoteReq" + "$ref": "#/definitions/handler.RespBody" } } + } + } + }, + "/answer/api/v1/personal/rank/page": { + "get": { + "description": "user personal rank list", + "produces": [ + "application/json" + ], + "tags": [ + "Rank" + ], + "summary": "user personal rank list", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "username", + "name": "username", + "in": "query" + } ], "responses": { "200": { @@ -3768,7 +3966,22 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.VoteResp" + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRankPersonalPageResp" + } + } + } + } + ] } } } @@ -3778,14 +3991,14 @@ } } }, - "/answer/api/v1/vote/up": { - "post": { + "/answer/api/v1/personal/user/info": { + "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "add vote", + "description": "GetOtherUserInfoByUsername", "consumes": [ "application/json" ], @@ -3793,18 +4006,16 @@ "application/json" ], "tags": [ - "Activity" + "User" ], - "summary": "vote up", + "summary": "GetOtherUserInfoByUsername", "parameters": [ { - "description": "vote", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.VoteReq" - } + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true } ], "responses": { @@ -3819,7 +4030,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.VoteResp" + "$ref": "#/definitions/schema.GetOtherUserInfoResp" } } } @@ -3829,14 +4040,14 @@ } } }, - "/personal/question/page": { + "/answer/api/v1/personal/vote/page": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "UserList", + "description": "get user personal votes", "consumes": [ "application/json" ], @@ -3844,51 +4055,3445 @@ "application/json" ], "tags": [ - "api-question" + "Activity" ], - "summary": "UserList", + "summary": "get user personal votes", "parameters": [ { - "type": "string", - "default": "string", - "description": "username", - "name": "username", - "in": "query", - "required": true - }, - { - "enum": [ - "newest", - "score" - ], - "type": "string", - "description": "order", - "name": "order", - "in": "query", - "required": true - }, - { - "type": "string", - "default": "0", - "description": "page", + "type": "integer", + "description": "page size", "name": "page", - "in": "query", - "required": true + "in": "query" }, { - "type": "string", - "default": "20", - "description": "pagesize", - "name": "pagesize", - "in": "query", - "required": true + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetVoteWithPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/plugin/status": { + "get": { + "description": "get all plugins status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Plugin" + ], + "summary": "get all plugins status", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetPluginListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/post/render": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "render post content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Upload" + ], + "summary": "render post content", + "parameters": [ + { + "description": "PostRenderReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.PostRenderReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAdd" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "delete question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RemoveQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/answer": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add question and answer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "add question and answer", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionAddByAnswer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/info": { + "get": { + "description": "get question details", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question details", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question TagID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/question/invite": { + "get": { + "description": "get question invite user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get question invite user info", + "parameters": [ + { + "type": "string", + "default": "1", + "description": "Question ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update question invite user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "update question invite user", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionUpdateInviteUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/link": { + "get": { + "description": "get question link", + "tags": [ + "Question" + ], + "summary": "get question link", + "parameters": [ + { + "minimum": 1, + "type": "integer", + "name": "in_days", + "in": "query" + }, + { + "enum": [ + "newest", + "active", + "hot", + "score", + "unanswered", + "recommend", + "frequent" + ], + "type": "string", + "name": "order", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "name": "question_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/operation": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Operation question \\n operation [pin unpin hide show]", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Operation question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.OperationQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/page": { + "get": { + "description": "get questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/question/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover deleted question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "recover deleted question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionRecoverReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/reopen": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "reopen question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "reopen question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReopenQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/similar": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "fuzzy query similar questions based on title", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "fuzzy query similar questions based on title", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "title", + "name": "title", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/similar/tag": { + "get": { + "description": "Search Similar Question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Search Similar Question", + "parameters": [ + { + "type": "string", + "default": "", + "description": "question_id", + "name": "question_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/question/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Close question", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "Close question", + "parameters": [ + { + "description": "question", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.CloseQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/question/tags": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get tag list", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag list", + "parameters": [ + { + "type": "string", + "description": "tag", + "name": "tag", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/reasons": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get reasons by object type and action", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "reason" + ], + "summary": "get reasons by object type and action", + "parameters": [ + { + "enum": [ + "question", + "answer", + "comment", + "user" + ], + "type": "string", + "description": "object_type", + "name": "object_type", + "in": "query", + "required": true + }, + { + "enum": [ + "status", + "close", + "flag", + "review" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/render/config": { + "get": { + "description": "GetRenderConfig", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PluginRender" + ], + "summary": "GetRenderConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plugin.RenderConfig" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/report": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add report \u003cbr\u003e source (question, answer, comment, user)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "add report", + "parameters": [ + { + "description": "report", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/report/review": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "review report", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "review report", + "parameters": [ + { + "description": "flag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.ReviewReportReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/report/unreviewed/post": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed report post page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Report" + ], + "summary": "get unreviewed report post page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReportListPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/review/pending/post": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update review", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Review" + ], + "summary": "update review", + "parameters": [ + { + "description": "review", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateReviewReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/review/pending/post/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed post page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Review" + ], + "summary": "get unreviewed post page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "string", + "description": "object_id", + "name": "object_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedPostPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/reviewing/type": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get reviewing type", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get reviewing type", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetReviewingTypeResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions": { + "get": { + "description": "get revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get revision list", + "parameters": [ + { + "type": "string", + "description": "object id", + "name": "object_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/revisions/audit": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "revision audit operation:approve or reject", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "revision audit", + "parameters": [ + { + "description": "audit", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RevisionAuditReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/edit/check": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "check can update revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "check can update revision", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/revisions/unreviewed": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get unreviewed revision list", + "produces": [ + "application/json" + ], + "tags": [ + "Revision" + ], + "summary": "get unreviewed revision list", + "parameters": [ + { + "type": "string", + "description": "page id", + "name": "page", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUnreviewedRevisionResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "search object", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "search object", + "parameters": [ + { + "type": "string", + "description": "query string", + "name": "q", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "active", + "score", + "relevance" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SearchResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/search/desc": { + "get": { + "description": "get search description", + "produces": [ + "application/json" + ], + "tags": [ + "Search" + ], + "summary": "get search description", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SearchResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/siteinfo": { + "get": { + "description": "get site info", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site info", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.SiteInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/siteinfo/legal": { + "get": { + "description": "get site legal info", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site legal info", + "parameters": [ + { + "enum": [ + "tos", + "privacy" + ], + "type": "string", + "description": "legal information type", + "name": "info_type", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetSiteLegalInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tag": { + "get": { + "description": "get tag one", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag one", + "parameters": [ + { + "type": "string", + "description": "tag id", + "name": "tag_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tag name", + "name": "tag_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetTagResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "update tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "add tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "delete tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RemoveTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/merge": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "merge tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "merge tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.AddTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/recover": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "recover delete tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "recover delete tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.RecoverTagReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/synonym": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update tag", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "update tag", + "parameters": [ + { + "description": "tag", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateTagSynonymReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/tag/synonyms": { + "get": { + "description": "get tag synonyms", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag synonyms", + "parameters": [ + { + "type": "integer", + "description": "tag id", + "name": "tag_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetTagSynonymsResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags": { + "get": { + "description": "get tags list by slug name", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tags list", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "string collection", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags/following": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get following tag list", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get following tag list", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetFollowingTagsResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/tags/page": { + "get": { + "description": "get tag page", + "produces": [ + "application/json" + ], + "tags": [ + "Tag" + ], + "summary": "get tag page", + "parameters": [ + { + "type": "integer", + "description": "page size", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "slug_name", + "name": "slug_name", + "in": "query" + }, + { + "enum": [ + "popular", + "name", + "newest" + ], + "type": "string", + "description": "query condition", + "name": "query_cond", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/action/record": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "ActionRecord", + "tags": [ + "User" + ], + "summary": "ActionRecord", + "parameters": [ + { + "enum": [ + "login", + "e_mail", + "find_pass" + ], + "type": "string", + "description": "action", + "name": "action", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.ActionRecordResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/email": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user change email verification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user change email verification", + "parameters": [ + { + "description": "UserChangeEmailVerifyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserChangeEmailVerifyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/email/change/code": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "send email to the user email then change their email", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "send email to the user email then change their email", + "parameters": [ + { + "description": "UserChangeEmailSendCodeReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserChangeEmailSendCodeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/email/verification": { + "post": { + "description": "UserVerifyEmail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserVerifyEmail", + "parameters": [ + { + "type": "string", + "default": "", + "description": "code", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/email/verification/send": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserVerifyEmailSend", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserVerifyEmailSend", + "parameters": [ + { + "type": "string", + "default": "", + "description": "captcha_id", + "name": "captcha_id", + "in": "query" + }, + { + "type": "string", + "default": "", + "description": "captcha_code", + "name": "captcha_code", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user info, if user no login response http code is 200, but user info is null", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "GetUserInfoByUserID", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetCurrentLoginUserInfoResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInfo update user info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInfo update user info", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateInfoRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/info/search": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "SearchUserListByName", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "SearchUserListByName", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetOtherUserInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/login/email": { + "post": { + "description": "UserEmailLogin", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserEmailLogin", + "parameters": [ + { + "description": "UserEmailLogin", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserEmailLoginReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/logout": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "user logout", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "user logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/notification/config": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "update user's notification config", + "parameters": [ + { + "description": "UpdateUserNotificationConfigReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserNotificationConfigReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + }, + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user's notification config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user's notification config", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserNotificationConfigResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/notification/unsubscribe": { + "put": { + "description": "unsubscribe notification", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "unsubscribe notification", + "parameters": [ + { + "description": "UserUnsubscribeNotificationReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserUnsubscribeNotificationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/password": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserModifyPassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserModifyPassWord", + "parameters": [ + { + "description": "UserModifyPasswordReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserModifyPasswordReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/password/replacement": { + "post": { + "description": "UseRePassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UseRePassWord", + "parameters": [ + { + "description": "UserRePassWordRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRePassWordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/password/reset": { + "post": { + "description": "RetrievePassWord", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "RetrievePassWord", + "parameters": [ + { + "description": "UserRetrievePassWordRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRetrievePassWordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/answer/api/v1/user/plugin/config": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user plugin config", + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "get user plugin config", + "parameters": [ + { + "type": "string", + "description": "plugin_slug_name", + "name": "plugin_slug_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetPluginConfigResp" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update user plugin config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "update user plugin config", + "parameters": [ + { + "description": "UpdatePluginConfigReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserPluginConfigReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/api/v1/user/plugin/configs": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get plugin list that used for user.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "UserPlugin" + ], + "summary": "get plugin list that used for user.", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserPluginListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/ranking": { + "get": { + "description": "get user ranking", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user ranking", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserRankingResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/register/email": { + "post": { + "description": "UserRegisterByEmail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserRegisterByEmail", + "parameters": [ + { + "description": "UserRegisterReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UserRegisterReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.UserLoginResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/user/staff": { + "get": { + "description": "get user staff", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "get user staff", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetUserStaffResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/vote/down": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add vote", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Activity" + ], + "summary": "vote down", + "parameters": [ + { + "description": "vote", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.VoteReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.VoteResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/vote/up": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "add vote", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Activity" + ], + "summary": "vote up", + "parameters": [ + { + "description": "vote", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.VoteReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.VoteResp" + } + } + } + ] + } + } + } + } + }, + "/custom.css": { + "get": { + "description": "get site custom CSS", + "produces": [ + "text/css" + ], + "tags": [ + "site" + ], + "summary": "get site custom CSS", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + } + } + } + }, + "/installation/base-info": { + "post": { + "description": "init base info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "init base info", + "parameters": [ + { + "description": "InitBaseInfoReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.InitBaseInfoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/config-file/check": { + "post": { + "description": "check config file if exist when installation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "check config file if exist when installation", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/install.CheckConfigFileResp" + } + } + } + ] + } + } + } + } + }, + "/installation/db/check": { + "post": { + "description": "check database if exist when installation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "check database if exist when installation", + "parameters": [ + { + "description": "CheckDatabaseReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.CheckDatabaseReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/install.CheckConfigFileResp" + } + } + } + ] + } + } + } + } + }, + "/installation/init": { + "post": { + "description": "init environment", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "installation" + ], + "summary": "init environment", + "parameters": [ + { + "description": "CheckDatabaseReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/install.CheckDatabaseReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/language/config": { + "get": { + "description": "get installation language config mapping", + "produces": [ + "application/json" + ], + "tags": [ + "Lang" + ], + "summary": "get installation language config mapping", + "parameters": [ + { + "type": "string", + "description": "installation language", + "name": "lang", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/installation/language/options": { + "get": { + "description": "get installation language options", + "produces": [ + "application/json" + ], + "tags": [ + "Lang" + ], + "summary": "get installation language options", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/translator.LangOption" + } + } + } + } + ] + } + } + } + } + }, + "/personal/question/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list personal questions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Personal" + ], + "summary": "list personal questions", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "username", + "name": "username", + "in": "query", + "required": true + }, + { + "enum": [ + "newest", + "score" + ], + "type": "string", + "description": "order", + "name": "order", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "0", + "description": "page", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "string", + "default": "20", + "description": "page_size", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/robots.txt": { + "get": { + "description": "get site robots information", + "produces": [ + "application/json" + ], + "tags": [ + "site" + ], + "summary": "get site robots information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" } } } @@ -3896,1396 +7501,3792 @@ } }, "definitions": { - "entity.AdminSetAnswerStatusRequest": { + "constant.NotificationChannelKey": { + "type": "string", + "enum": [ + "email" + ], + "x-enum-varnames": [ + "EmailChannel" + ] + }, + "constant.Privilege": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "value": { + "type": "integer", + "minimum": 1 + } + } + }, + "entity.BadgeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BadgeLevelBronze", + "BadgeLevelSilver", + "BadgeLevelGold" + ] + }, + "handler.RespBody": { + "type": "object", + "properties": { + "code": { + "description": "http code", + "type": "integer" + }, + "data": { + "description": "response data" + }, + "msg": { + "description": "response message", + "type": "string" + }, + "reason": { + "description": "reason key", + "type": "string" + } + } + }, + "install.CheckConfigFileResp": { + "type": "object", + "properties": { + "config_file_exist": { + "type": "boolean" + }, + "db_connection_success": { + "type": "boolean" + }, + "db_table_exist": { + "type": "boolean" + } + } + }, + "install.CheckDatabaseReq": { + "type": "object", + "required": [ + "db_type" + ], + "properties": { + "db_file": { + "type": "string" + }, + "db_host": { + "type": "string" + }, + "db_name": { + "type": "string" + }, + "db_password": { + "type": "string" + }, + "db_type": { + "type": "string", + "enum": [ + "postgres", + "sqlite3", + "mysql" + ] + }, + "db_username": { + "type": "string" + }, + "ssl_cert": { + "type": "string" + }, + "ssl_enabled": { + "type": "boolean" + }, + "ssl_key": { + "type": "string" + }, + "ssl_mode": { + "type": "string" + }, + "ssl_root_cert": { + "type": "string" + } + } + }, + "install.InitBaseInfoReq": { + "type": "object", + "required": [ + "contact_email", + "email", + "external_content_display", + "lang", + "name", + "password", + "site_name", + "site_url" + ], + "properties": { + "contact_email": { + "type": "string", + "maxLength": 500 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "lang": { + "type": "string", + "maxLength": 30 + }, + "login_required": { + "type": "boolean" + }, + "name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "site_name": { + "type": "string", + "maxLength": 30 + }, + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "pager.PageModel": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "list": {} + } + }, + "plugin.EmbedConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "platform": { + "type": "string" + } + } + }, + "plugin.RenderConfig": { + "type": "object", + "properties": { + "select_theme": { + "type": "string" + } + } + }, + "schema.AcceptAnswerReq": { + "type": "object", + "required": [ + "question_id" + ], + "properties": { + "answer_id": { + "type": "string" + }, + "question_id": { + "type": "string", + "maxLength": 30 + } + } + }, + "schema.ActObjectInfo": { + "type": "object", + "properties": { + "answer_id": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "main_tag_slug_name": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "schema.ActObjectTimeline": { + "type": "object", + "properties": { + "activity_id": { + "type": "string" + }, + "activity_type": { + "type": "string" + }, + "cancelled": { + "type": "boolean" + }, + "cancelled_at": { + "type": "integer" + }, + "comment": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "revision_id": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + } + } + }, + "schema.ActionRecordResp": { + "type": "object", + "properties": { + "captcha_id": { + "type": "string" + }, + "captcha_img": { + "type": "string" + }, + "verify": { + "type": "boolean" + } + } + }, + "schema.AddCommentReq": { + "type": "object", + "required": [ + "object_id", + "original_text" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "mention_username_list": { + "description": "@ user id list", + "type": "array", + "items": { + "type": "string" + } + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "original_text": { + "description": "original comment content", + "type": "string", + "maxLength": 600, + "minLength": 2 + }, + "reply_comment_id": { + "description": "reply comment id", + "type": "string" + } + } + }, + "schema.AddReportReq": { + "type": "object", + "required": [ + "object_id", + "report_type" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, + "content": { + "description": "report content", + "type": "string", + "maxLength": 500 + }, + "object_id": { + "description": "object id", + "type": "string", + "maxLength": 20 + }, + "report_type": { + "description": "report type", + "type": "integer" + } + } + }, + "schema.AddTagReq": { + "type": "object", + "required": [ + "display_name", + "original_text", + "slug_name" + ], + "properties": { + "display_name": { + "description": "display_name", + "type": "string", + "maxLength": 35 + }, + "original_text": { + "description": "original text", + "type": "string", + "maxLength": 65536 + }, + "slug_name": { + "description": "slug_name", + "type": "string", + "maxLength": 35 + } + } + }, + "schema.AddUserReq": { + "type": "object", + "required": [ + "display_name", + "email", + "password" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.AddUsersReq": { + "type": "object", + "properties": { + "users": { + "description": "users info line by line", + "type": "string" + } + } + }, + "schema.AdminUpdateAnswerStatusReq": { + "type": "object", + "required": [ + "answer_id", + "status" + ], + "properties": { + "answer_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "available", + "deleted" + ] + } + } + }, + "schema.AdminUpdateQuestionStatusReq": { + "type": "object", + "required": [ + "question_id", + "status" + ], + "properties": { + "question_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "available", + "closed", + "deleted" + ] + } + } + }, + "schema.AnswerAddReq": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "question_id": { + "type": "string" + } + } + }, + "schema.AnswerInfo": { + "type": "object", + "properties": { + "accepted": { + "type": "integer" + }, + "collected": { + "type": "boolean" + }, + "content": { + "type": "string" + }, + "create_time": { + "type": "integer" + }, + "html": { + "type": "string" + }, + "id": { + "type": "string" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "question_id": { + "type": "string" + }, + "question_info": { + "$ref": "#/definitions/schema.QuestionInfoResp" + }, + "status": { + "type": "integer" + }, + "update_time": { + "type": "integer" + }, + "update_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "vote_count": { + "type": "integer" + }, + "vote_status": { + "type": "string" + } + } + }, + "schema.AnswerUpdateReq": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "edit_summary": { + "type": "string" + }, + "id": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "schema.AvatarInfo": { + "type": "object", + "properties": { + "custom": { + "type": "string", + "maxLength": 200 + }, + "gravatar": { + "type": "string", + "maxLength": 200 + }, + "type": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.BadgeListInfo": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "earned_count": { + "description": "badge earned count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.BadgeStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ], + "x-enum-varnames": [ + "BadgeStatusActive", + "BadgeStatusInactive" + ] + }, + "schema.CloseQuestionReq": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "close_msg": { + "description": "close_type", + "type": "string" + }, + "close_type": { + "description": "close_type", + "type": "integer" + }, + "id": { + "type": "string" + } + } + }, + "schema.CollectionSwitchReq": { + "type": "object", + "required": [ + "group_id", + "object_id" + ], + "properties": { + "bookmark": { + "type": "boolean" + }, + "group_id": { + "type": "string" + }, + "object_id": { + "type": "string" + } + } + }, + "schema.CollectionSwitchResp": { + "type": "object", + "properties": { + "object_collection_count": { + "type": "integer" + } + } + }, + "schema.ConfigField": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ConfigFieldOption" + } + }, + "required": { + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "ui_options": { + "$ref": "#/definitions/schema.ConfigFieldUIOptions" + }, + "value": {} + } + }, + "schema.ConfigFieldOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "schema.ConfigFieldUIOptions": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/schema.UIOptionAction" + }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, + "input_type": { + "type": "string" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "rows": { + "type": "string" + }, + "text": { + "type": "string" + }, + "variant": { + "type": "string" + } + } + }, + "schema.ConnectorInfoResp": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "schema.ConnectorUserInfoResp": { + "type": "object", + "properties": { + "binding": { + "type": "boolean" + }, + "external_id": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, + "schema.EditUserProfileReq": { + "type": "object", + "required": [ + "display_name", + "email", + "user_id" + ], + "properties": { + "display_name": { + "type": "string", + "maxLength": 30, + "minLength": 2 + }, + "email": { + "type": "string", + "maxLength": 500 + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string", + "maxLength": 30, + "minLength": 2 + } + } + }, + "schema.ExternalLoginBindingUserSendEmailReq": { + "type": "object", + "required": [ + "binding_key", + "email" + ], + "properties": { + "binding_key": { + "type": "string", + "maxLength": 100 + }, + "email": { + "type": "string", + "maxLength": 512 + }, + "must": { + "description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.", + "type": "boolean" + } + } + }, + "schema.ExternalLoginBindingUserSendEmailResp": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "email_exist_and_must_be_confirmed": { + "type": "boolean" + } + } + }, + "schema.ExternalLoginUnbindingReq": { + "type": "object", + "required": [ + "external_id" + ], + "properties": { + "external_id": { + "type": "string", + "maxLength": 128 + } + } + }, + "schema.FollowReq": { + "type": "object", + "required": [ + "object_id" + ], + "properties": { + "is_cancel": { + "description": "is cancel", + "type": "boolean" + }, + "object_id": { + "description": "object id", + "type": "string" + } + } + }, + "schema.FollowResp": { + "type": "object", + "properties": { + "follows": { + "description": "the followers of object", + "type": "integer" + }, + "is_followed": { + "description": "if user is followed object will be true,otherwise false", + "type": "boolean" + } + } + }, + "schema.GetAnswerInfoResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.AnswerInfo" + }, + "question": { + "$ref": "#/definitions/schema.QuestionInfoResp" + } + } + }, + "schema.GetBadgeInfoResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned_count": { + "description": "badge earned count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "is_single": { + "description": "badge is single or multiple", + "type": "boolean" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.GetBadgeListPagedResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned": { + "description": "badge earned count", + "type": "boolean" + }, + "group_name": { + "description": "badge group name", + "type": "string" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, + "schema.GetBadgeListResp": { + "type": "object", + "properties": { + "badges": { + "description": "badge list info", + "type": "array", + "items": { + "$ref": "#/definitions/schema.BadgeListInfo" + } + }, + "group_name": { + "description": "badge group name", + "type": "string" + } + } + }, + "schema.GetCommentPersonalWithPageResp": { + "type": "object", + "properties": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "comment_id": { + "description": "comment id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + } + } + }, + "schema.GetCommentResp": { + "type": "object", + "properties": { + "comment_id": { + "description": "comment id", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "is_vote": { + "description": "current user if already vote this comment", + "type": "boolean" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "original_text": { + "description": "original comment content", + "type": "string" + }, + "parsed_text": { + "description": "parsed comment content", + "type": "string" + }, + "reply_comment_id": { + "description": "reply comment id", + "type": "string" + }, + "reply_user_display_name": { + "description": "reply user display name", + "type": "string" + }, + "reply_user_id": { + "description": "reply user id", + "type": "string" + }, + "reply_user_status": { + "description": "reply user status", + "type": "string" + }, + "reply_username": { + "description": "reply user username", + "type": "string" + }, + "user_avatar": { + "description": "user avatar", + "type": "string" + }, + "user_display_name": { + "description": "user display name", + "type": "string" + }, + "user_id": { + "description": "user id", + "type": "string" + }, + "user_status": { + "description": "user status", + "type": "string" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "user vote amount", + "type": "integer" + } + } + }, + "schema.GetCurrentLoginUserInfoResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "$ref": "#/definitions/schema.AvatarInfo" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "have_password": { + "description": "user have password", + "type": "boolean" + }, + "id": { + "description": "user id", + "type": "string" + }, + "language": { + "description": "language", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "visit_token": { + "description": "visit token", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, + "schema.GetFollowingTagsResp": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.GetObjectTimelineResp": { + "type": "object", + "properties": { + "object_info": { + "$ref": "#/definitions/schema.ActObjectInfo" + }, + "timeline": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ActObjectTimeline" + } + } + } + }, + "schema.GetOtherUserInfoByUsernameResp": { + "type": "object", + "properties": { + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "avatar": { + "description": "avatar", + "type": "string" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "follow_count": { + "description": "email\nfollow count", + "type": "integer" + }, + "id": { + "description": "user id", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "status": { + "type": "string" + }, + "status_msg": { + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, + "schema.GetOtherUserInfoResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" + } + } + }, + "schema.GetPluginConfigResp": { + "type": "object", + "properties": { + "config_fields": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ConfigField" + } + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug_name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "schema.GetPluginListResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "have_config": { + "type": "boolean" + }, + "link": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug_name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "schema.GetPrivilegesConfigResp": { + "type": "object", + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PrivilegeOption" + } + }, + "selected_level": { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + } + }, + "schema.GetRankPersonalPageResp": { + "type": "object", + "properties": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "rank_type": { + "description": "rank type", + "type": "string" + }, + "reputation": { + "description": "reputation", + "type": "integer" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + } + } + }, + "schema.GetReportListPageResp": { + "type": "object", + "properties": { + "answer_accepted": { + "type": "boolean" + }, + "answer_count": { + "type": "integer" + }, + "answer_id": { + "type": "string" + }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "flag_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "$ref": "#/definitions/schema.ReasonItem" + }, + "reason_content": { + "type": "string" + }, + "submit_at": { + "type": "integer" + }, + "submitter_user": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetReviewingTypeResp": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "name": { + "type": "string" + }, + "todo_amount": { + "type": "integer" + } + } + }, + "schema.GetRevisionResp": { + "type": "object", + "properties": { + "content": {}, + "create_at": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + }, + "use_id": { + "type": "string" + }, + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + } + } + }, + "schema.GetRoleResp": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "schema.GetSMTPConfigResp": { + "type": "object", + "properties": { + "encryption": { + "description": "\"\" SSL TLS", + "type": "string" + }, + "from_email": { + "type": "string" + }, + "from_name": { + "type": "string" + }, + "smtp_authentication": { + "type": "boolean" + }, + "smtp_host": { + "type": "string" + }, + "smtp_password": { + "type": "string" + }, + "smtp_port": { + "type": "integer" + }, + "smtp_username": { + "type": "string" + } + } + }, + "schema.GetSiteLegalInfoResp": { + "type": "object", + "properties": { + "privacy_policy_original_text": { + "type": "string" + }, + "privacy_policy_parsed_text": { + "type": "string" + }, + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { + "type": "string" + } + } + }, + "schema.GetTagBasicResp": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + }, + "tag_id": { + "type": "string" + } + } + }, + "schema.GetTagPageResp": { + "type": "object", + "properties": { + "created_at": { + "description": "created time", + "type": "integer" + }, + "description": { + "description": "description", + "type": "string" + }, + "display_name": { + "description": "display_name", + "type": "string" + }, + "excerpt": { + "description": "excerpt", + "type": "string" + }, + "follow_count": { + "description": "follower amount", + "type": "integer" + }, + "is_follower": { + "description": "is follower", + "type": "boolean" + }, + "original_text": { + "description": "original text", + "type": "string" + }, + "parsed_text": { + "description": "parsed_text", + "type": "string" + }, + "question_count": { + "description": "question amount", + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "description": "slug_name", + "type": "string" + }, + "tag_id": { + "description": "tag_id", + "type": "string" + }, + "updated_at": { + "description": "updated time", + "type": "integer" + } + } + }, + "schema.GetTagResp": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "excerpt": { + "type": "string" + }, + "follow_count": { + "type": "integer" + }, + "is_follower": { + "type": "boolean" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "member_actions": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_count": { + "type": "integer" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tag_id": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "schema.GetTagSynonymsResp": { + "type": "object", + "properties": { + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "synonyms": { + "description": "synonyms", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagSynonym" + } + } + } + }, + "schema.GetUnreviewedPostPageResp": { "type": "object", "properties": { "answer_id": { "type": "string" }, + "author_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "comment_id": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "object_id": { + "type": "string" + }, + "object_show_status": { + "type": "integer" + }, + "object_status": { + "type": "integer" + }, + "object_type": { + "type": "string", + "enum": [ + "question", + "answer", + "comment" + ] + }, + "original_text": { + "type": "string" + }, + "parsed_text": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "review_id": { + "type": "integer" + }, + "submit_at": { + "type": "integer" + }, + "submitter_display_name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" + } + } + }, + "schema.GetUnreviewedRevisionResp": { + "type": "object", + "properties": { + "info": { + "$ref": "#/definitions/schema.UnreviewedRevisionInfoInfo" + }, + "type": { + "type": "string" + }, + "unreviewed_info": { + "$ref": "#/definitions/schema.GetRevisionResp" + } + } + }, + "schema.GetUserActivationResp": { + "type": "object", + "properties": { + "activation_url": { + "type": "string" + } + } + }, + "schema.GetUserBadgeAwardListResp": { + "type": "object", + "properties": { + "earned_count": { + "description": "badge award count", + "type": "integer" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + } + } + }, + "schema.GetUserNotificationConfigResp": { + "type": "object", + "properties": { + "all_new_question": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "all_new_question_for_following_tags": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "inbox": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + }, + "schema.GetUserPageResp": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "deleted_at": { + "description": "delete time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "role_name": { + "description": "role name", + "type": "string" + }, "status": { + "description": "user status(normal,suspended,deleted,inactive)", + "type": "string" + }, + "suspended_at": { + "description": "suspended time", + "type": "integer" + }, + "suspended_until": { + "description": "suspended until time", + "type": "integer" + }, + "user_id": { + "description": "user id", + "type": "string" + }, + "username": { + "description": "username", "type": "string" } } }, - "handler.RespBody": { + "schema.GetUserPluginListResp": { "type": "object", "properties": { - "code": { - "description": "http code", - "type": "integer" + "name": { + "type": "string" }, - "data": { - "description": "response data" + "slug_name": { + "type": "string" + } + } + }, + "schema.GetUserStaffResp": { + "type": "object", + "properties": { + "avatar": { + "description": "avatar", + "type": "string" }, - "msg": { - "description": "response message", + "display_name": { + "description": "display name", "type": "string" }, - "reason": { - "description": "reason key", + "username": { + "description": "username", "type": "string" } } }, - "pager.PageModel": { + "schema.GetVoteWithPageResp": { "type": "object", "properties": { - "count": { + "answer_id": { + "description": "answer id", + "type": "string" + }, + "content": { + "description": "content", + "type": "string" + }, + "created_at": { + "description": "create time", "type": "integer" }, - "list": {} + "object_id": { + "description": "object id", + "type": "string" + }, + "object_type": { + "description": "object type", + "type": "string", + "enum": [ + "question", + "answer", + "tag", + "comment" + ] + }, + "question_id": { + "description": "question id", + "type": "string" + }, + "title": { + "description": "title", + "type": "string" + }, + "url_title": { + "description": "url title", + "type": "string" + }, + "vote_type": { + "description": "vote type", + "type": "string" + } } }, - "schema.ActionRecordResp": { + "schema.LoadingAction": { "type": "object", "properties": { - "captcha_id": { + "state": { "type": "string" }, - "captcha_img": { + "text": { "type": "string" - }, - "verify": { + } + } + }, + "schema.NotificationChannelConfig": { + "type": "object", + "properties": { + "enable": { "type": "boolean" + }, + "key": { + "$ref": "#/definitions/constant.NotificationChannelKey" } } }, - "schema.AddCommentReq": { + "schema.NotificationClearIDRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "schema.NotificationClearRequest": { "type": "object", "required": [ - "object_id", - "original_text", - "parsed_text" + "type" ], "properties": { - "mention_username_list": { - "description": "@ user id list", - "type": "array", - "items": { - "type": "string" - } + "type": { + "type": "string", + "enum": [ + "inbox", + "achievement" + ] + } + } + }, + "schema.OnCompleteAction": { + "type": "object", + "properties": { + "refresh_form_config": { + "type": "boolean" }, - "object_id": { - "description": "object id", + "toast_return_message": { + "type": "boolean" + } + } + }, + "schema.Operation": { + "type": "object", + "properties": { + "description": { "type": "string" }, - "original_text": { - "description": "original comment content", - "type": "string" + "level": { + "$ref": "#/definitions/schema.OperationLevel" }, - "parsed_text": { - "description": "parsed comment content", + "msg": { "type": "string" }, - "reply_comment_id": { - "description": "reply comment id", + "time": { + "type": "integer" + }, + "type": { "type": "string" } } }, - "schema.AddReportReq": { + "schema.OperationLevel": { + "type": "string", + "enum": [ + "info", + "danger", + "warning", + "secondary" + ], + "x-enum-varnames": [ + "OperationLevelInfo", + "OperationLevelDanger", + "OperationLevelWarning", + "OperationLevelSecondary" + ] + }, + "schema.OperationQuestionReq": { "type": "object", "required": [ - "object_id", - "report_type" + "id" ], "properties": { - "content": { - "description": "report content", - "type": "string", - "maxLength": 500 + "id": { + "type": "string" }, - "object_id": { - "description": "object id", - "type": "string", - "maxLength": 20 + "operation": { + "description": "operation [pin unpin hide show]", + "type": "string" + } + } + }, + "schema.PermissionMemberAction": { + "type": "object", + "properties": { + "action": { + "type": "string" }, - "report_type": { - "description": "report type", - "type": "integer" + "name": { + "type": "string" + }, + "type": { + "type": "string" } } }, - "schema.AdminSetQuestionStatusRequest": { + "schema.PostRenderReq": { "type": "object", "properties": { - "question_id": { + "content": { "type": "string" + } + } + }, + "schema.PrivilegeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 99 + ], + "x-enum-varnames": [ + "PrivilegeLevel1", + "PrivilegeLevel2", + "PrivilegeLevel3", + "PrivilegeLevelCustom" + ] + }, + "schema.PrivilegeOption": { + "type": "object", + "properties": { + "level": { + "$ref": "#/definitions/schema.PrivilegeLevel" }, - "status": { + "level_desc": { "type": "string" + }, + "privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } } } }, - "schema.AnswerAddReq": { + "schema.QuestionAdd": { "type": "object", + "required": [ + "content", + "tags", + "title" + ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "captcha_id", + "type": "string" + }, "content": { "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, + "schema.QuestionAddByAnswer": { + "type": "object", + "required": [ + "answer_content", + "content", + "tags", + "title" + ], + "properties": { + "answer_content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "captcha_code": { "type": "string" }, - "html": { - "description": "html", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "question_id": { - "description": "question_id", - "type": "string" - } - } - }, - "schema.AnswerAdoptedReq": { - "type": "object", - "properties": { - "answer_id": { - "type": "string" + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "mention_username_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } }, - "question_id": { - "description": "question_id", - "type": "string" + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, - "schema.AnswerList": { + "schema.QuestionInfoResp": { "type": "object", "properties": { - "order": { - "description": "1 Default 2 time", + "accepted_answer_id": { "type": "string" }, - "page": { - "description": "Query number of pages", + "answer_count": { "type": "integer" }, - "page_size": { - "description": "Search page size", + "answered": { + "type": "boolean" + }, + "collected": { + "type": "boolean" + }, + "collection_count": { "type": "integer" }, - "question_id": { - "description": "question_id", - "type": "string" - } - } - }, - "schema.AnswerUpdateReq": { - "type": "object", - "properties": { "content": { - "description": "content", "type": "string" }, - "edit_summary": { - "description": "edit_summary", + "create_time": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "edit_time": { + "type": "integer" + }, + "extends_actions": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "first_answer_id": { "type": "string" }, + "follow_count": { + "type": "integer" + }, "html": { - "description": "html", "type": "string" }, "id": { - "description": "id", "type": "string" }, - "question_id": { - "description": "question_id", - "type": "string" + "is_followed": { + "type": "boolean" }, - "title": { - "description": "title", - "type": "string" - } - } - }, - "schema.CloseQuestionReq": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "close_msg": { - "description": "close_type", + "last_answer_id": { "type": "string" }, - "close_type": { - "description": "close_type", + "last_answered_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" + }, + "member_actions": { + "description": "MemberActions", + "type": "array", + "items": { + "$ref": "#/definitions/schema.PermissionMemberAction" + } + }, + "operation": { + "$ref": "#/definitions/schema.Operation" + }, + "pin": { "type": "integer" }, - "id": { - "type": "string" - } - } - }, - "schema.CollectionSwitchReq": { - "type": "object", - "required": [ - "group_id", - "object_id" - ], - "properties": { - "group_id": { - "description": "user collection group TagID", - "type": "string" + "show": { + "type": "integer" }, - "object_id": { - "description": "object TagID", - "type": "string" - } - } - }, - "schema.CollectionSwitchResp": { - "type": "object", - "properties": { - "object_collection_count": { - "type": "string" + "status": { + "type": "integer" }, - "object_id": { + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { "type": "string" }, - "switch": { - "type": "boolean" - } - } - }, - "schema.FollowReq": { - "type": "object", - "required": [ - "object_id" - ], - "properties": { - "is_cancel": { - "description": "is cancel", - "type": "boolean" + "unique_view_count": { + "type": "integer" }, - "object_id": { - "description": "object id", - "type": "string" - } - } - }, - "schema.FollowResp": { - "type": "object", - "properties": { - "follows": { - "description": "the followers of object", + "update_time": { "type": "integer" }, - "is_followed": { - "description": "if user is followed object will be true,otherwise false", - "type": "boolean" - } - } - }, - "schema.GetCommentPersonalWithPageResp": { - "type": "object", - "properties": { - "answer_id": { - "description": "answer id", - "type": "string" + "update_user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" }, - "comment_id": { - "description": "comment id", + "url_title": { "type": "string" }, - "content": { - "description": "content", - "type": "string" + "user_info": { + "$ref": "#/definitions/schema.UserBasicInfo" }, - "created_at": { - "description": "create time", + "view_count": { "type": "integer" }, - "object_id": { - "description": "object id", + "vote_count": { + "type": "integer" + }, + "vote_status": { "type": "string" + } + } + }, + "schema.QuestionPageReq": { + "type": "object", + "properties": { + "in_days": { + "type": "integer", + "minimum": 1 }, - "object_type": { - "description": "object type", + "order": { "type": "string", "enum": [ - "question", - "answer", - "tag", - "comment" + "newest", + "active", + "hot", + "score", + "unanswered", + "recommend", + "frequent" ] }, - "question_id": { - "description": "question id", - "type": "string" + "page": { + "type": "integer", + "minimum": 1 }, - "title": { - "description": "title", - "type": "string" + "page_size": { + "type": "integer", + "minimum": 1 + }, + "tag": { + "type": "string", + "maxLength": 100 + }, + "username": { + "type": "string", + "maxLength": 100 } } }, - "schema.GetCommentResp": { + "schema.QuestionPageResp": { "type": "object", "properties": { - "comment_id": { - "description": "comment id", + "accepted_answer_id": { + "description": "answer information", "type": "string" }, - "created_at": { - "description": "create time", + "answer_count": { "type": "integer" }, - "is_vote": { - "description": "current user if already vote this comment", - "type": "boolean" + "collection_count": { + "type": "integer" }, - "member_actions": { - "description": "MemberActions", - "type": "array", - "items": { - "$ref": "#/definitions/schema.PermissionMemberAction" - } + "created_at": { + "type": "integer" }, - "object_id": { - "description": "object id", + "description": { "type": "string" }, - "original_text": { - "description": "original comment content", - "type": "string" + "follow_count": { + "type": "integer" }, - "parsed_text": { - "description": "parsed comment content", + "id": { "type": "string" }, - "reply_comment_id": { - "description": "reply comment id", + "last_answer_id": { "type": "string" }, - "reply_user_display_name": { - "description": "reply user display name", - "type": "string" + "operated_at": { + "description": "operator information", + "type": "integer" }, - "reply_user_id": { - "description": "reply user id", + "operation_type": { "type": "string" }, - "reply_user_status": { - "description": "reply user status", - "type": "string" + "operator": { + "$ref": "#/definitions/schema.QuestionPageRespOperator" }, - "reply_username": { - "description": "reply user username", - "type": "string" + "pin": { + "description": "1: unpin, 2: pin", + "type": "integer" + }, + "show": { + "description": "0: show, 1: hide", + "type": "integer" + }, + "status": { + "type": "integer" }, - "user_avatar": { - "description": "user avatar", - "type": "string" + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } }, - "user_display_name": { - "description": "user display name", + "title": { "type": "string" }, - "user_id": { - "description": "user id", - "type": "string" + "unique_view_count": { + "type": "integer" }, - "user_status": { - "description": "user status", + "url_title": { "type": "string" }, - "username": { - "description": "username", - "type": "string" + "view_count": { + "description": "question statistical information", + "type": "integer" }, "vote_count": { - "description": "user vote amount", "type": "integer" } } }, - "schema.GetFollowingTagsResp": { + "schema.QuestionPageRespOperator": { "type": "object", "properties": { + "avatar": { + "type": "string" + }, "display_name": { - "description": "display name", "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "id": { "type": "string" }, - "slug_name": { - "description": "slug name", + "rank": { + "type": "integer" + }, + "status": { "type": "string" }, - "tag_id": { - "description": "tag id", + "username": { "type": "string" } } }, - "schema.GetOtherUserInfoByUsernameResp": { + "schema.QuestionRecoverReq": { "type": "object", + "required": [ + "question_id" + ], "properties": { - "answer_count": { - "description": "answer count", - "type": "integer" + "question_id": { + "type": "string" + } + } + }, + "schema.QuestionUpdate": { + "type": "object", + "required": [ + "content", + "id", + "tags", + "title" + ], + "properties": { + "captcha_code": { + "type": "string" }, - "avatar": { - "description": "avatar", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "bio": { - "description": "bio markdown", + "content": { + "description": "content", + "type": "string", + "maxLength": 65535, + "minLength": 6 + }, + "edit_summary": { + "description": "edit summary", "type": "string" }, - "bio_html": { - "description": "bio html", + "id": { + "description": "question id", "type": "string" }, - "created_at": { - "description": "create time", - "type": "integer" + "invite_user": { + "type": "array", + "items": { + "type": "string" + } }, - "display_name": { - "description": "display name", + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "title": { + "description": "question title", + "type": "string", + "maxLength": 150, + "minLength": 6 + } + } + }, + "schema.QuestionUpdateInviteUser": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "captcha_code": { "type": "string" }, - "follow_count": { - "description": "email\nfollow count", - "type": "integer" + "captcha_id": { + "description": "captcha_id", + "type": "string" }, "id": { - "description": "user id", "type": "string" }, - "ip_info": { - "description": "ip info", + "invite_user": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "schema.ReactionRespItem": { + "type": "object", + "properties": { + "count": { + "description": "Count is the number of users who reacted", + "type": "integer" + }, + "emoji": { + "description": "Emoji is the reaction emoji", "type": "string" }, - "is_admin": { - "description": "is admin", + "is_active": { + "description": "IsActive is if current user has reacted", "type": "boolean" }, - "last_login_date": { - "description": "last login date", - "type": "integer" - }, - "location": { - "description": "location", + "tooltip": { + "description": "Tooltip is the user's name who reacted", "type": "string" - }, - "mobile": { - "description": "mobile", + } + } + }, + "schema.ReasonItem": { + "type": "object", + "properties": { + "content_type": { "type": "string" }, - "question_count": { - "description": "question count", - "type": "integer" - }, - "rank": { - "description": "rank", - "type": "integer" + "description": { + "type": "string" }, - "status": { + "name": { "type": "string" }, - "status_msg": { + "placeholder": { "type": "string" }, - "username": { - "description": "username", + "reason_key": { "type": "string" }, - "website": { - "description": "website", + "reason_type": { + "type": "integer" + } + } + }, + "schema.RecoverAnswerReq": { + "type": "object", + "required": [ + "answer_id" + ], + "properties": { + "answer_id": { "type": "string" } } }, - "schema.GetOtherUserInfoResp": { + "schema.RecoverTagReq": { "type": "object", + "required": [ + "tag_id" + ], "properties": { - "has": { - "type": "boolean" - }, - "info": { - "$ref": "#/definitions/schema.GetOtherUserInfoByUsernameResp" + "tag_id": { + "type": "string" } } }, - "schema.GetRankPersonalWithPageResp": { + "schema.RemoveAnswerReq": { "type": "object", + "required": [ + "id" + ], "properties": { - "answer_id": { - "description": "answer id", + "captcha_code": { "type": "string" }, - "content": { - "description": "content", + "captcha_id": { "type": "string" }, - "created_at": { - "description": "create time", - "type": "integer" - }, - "object_id": { - "description": "object id", + "id": { "type": "string" - }, - "object_type": { - "description": "object type", - "type": "string", - "enum": [ - "question", - "answer", - "tag", - "comment" - ] - }, - "question_id": { - "description": "question id", + } + } + }, + "schema.RemoveCommentReq": { + "type": "object", + "required": [ + "comment_id" + ], + "properties": { + "captcha_code": { "type": "string" }, - "rank_type": { - "description": "rank type", + "captcha_id": { "type": "string" }, - "reputation": { - "description": "reputation", - "type": "integer" - }, - "title": { - "description": "title", + "comment_id": { + "description": "comment id", "type": "string" } } }, - "schema.GetReportTypeResp": { + "schema.RemoveQuestionReq": { "type": "object", + "required": [ + "id" + ], "properties": { - "content_type": { - "description": "content type", + "captcha_code": { "type": "string" }, - "description": { - "description": "report description", + "captcha_id": { + "description": "captcha_id", "type": "string" }, - "have_content": { - "description": "is have content", - "type": "boolean" - }, - "name": { - "description": "report name", + "id": { + "description": "question id", + "type": "string" + } + } + }, + "schema.RemoveTagReq": { + "type": "object", + "required": [ + "tag_id" + ], + "properties": { + "tag_id": { + "description": "tag_id", "type": "string" - }, - "source": { - "description": "report source", + } + } + }, + "schema.ReopenQuestionReq": { + "type": "object", + "properties": { + "question_id": { "type": "string" - }, - "type": { - "description": "report type", - "type": "integer" } } }, - "schema.GetRevisionResp": { + "schema.ReviewReportReq": { "type": "object", + "required": [ + "flag_id", + "operation_type" + ], "properties": { - "content": { - "description": "content parsed" + "close_msg": { + "type": "string" }, - "create_at": { + "close_type": { "type": "integer" }, - "id": { - "description": "id", - "type": "string" + "content": { + "type": "string", + "maxLength": 65535, + "minLength": 6 }, - "object_id": { - "description": "object id", + "flag_id": { "type": "string" }, - "reason": { - "type": "string" + "operation_type": { + "type": "string", + "enum": [ + "edit_post", + "close_post", + "delete_post", + "unlist_post", + "ignore_report" + ] }, - "status": { - "description": "revision status(normal: 1; delete 2)", - "type": "integer" + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } }, "title": { - "description": "title", - "type": "string" - }, - "use_id": { - "description": "user id", - "type": "string" - }, - "user_info": { - "$ref": "#/definitions/schema.UserBasicInfo" + "type": "string", + "maxLength": 150, + "minLength": 6 } } }, - "schema.GetSMTPConfigResp": { + "schema.RevisionAuditReq": { "type": "object", + "required": [ + "id", + "operation" + ], "properties": { - "encryption": { - "description": "\"\" SSL", - "type": "string" - }, - "from_email": { - "type": "string" - }, - "from_name": { - "type": "string" - }, - "smtp_authentication": { - "type": "boolean" - }, - "smtp_host": { - "type": "string" - }, - "smtp_password": { + "id": { + "description": "object id", "type": "string" }, - "smtp_port": { - "type": "integer" - }, - "smtp_username": { + "operation": { + "description": "approve or reject", "type": "string" } } }, - "schema.GetTagPageResp": { + "schema.SearchObject": { "type": "object", "properties": { - "created_at": { - "description": "created time", + "accepted": { + "type": "boolean" + }, + "answer_count": { "type": "integer" }, - "display_name": { - "description": "display_name", - "type": "string" + "created_at": { + "type": "integer" }, "excerpt": { - "description": "excerpt", "type": "string" }, - "follow_count": { - "description": "follower amount", - "type": "integer" - }, - "is_follower": { - "description": "is follower", - "type": "boolean" + "id": { + "type": "string" }, - "original_text": { - "description": "original text", + "question_id": { "type": "string" }, - "parsed_text": { - "description": "parsed_text", + "status": { + "description": "Status", "type": "string" }, - "question_count": { - "description": "question amount", - "type": "integer" + "tags": { + "description": "tags", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } }, - "slug_name": { - "description": "slug_name", + "title": { "type": "string" }, - "tag_id": { - "description": "tag_id", + "url_title": { "type": "string" }, - "updated_at": { - "description": "updated time", + "user_info": { + "description": "user info", + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObjectUser" + } + ] + }, + "vote_count": { "type": "integer" } } }, - "schema.GetTagResp": { + "schema.SearchObjectUser": { "type": "object", "properties": { - "created_at": { - "description": "created time", - "type": "integer" - }, "display_name": { - "description": "display name", "type": "string" }, - "excerpt": { - "description": "excerpt", + "id": { "type": "string" }, - "follow_count": { - "description": "follower amount", + "rank": { "type": "integer" }, - "is_follower": { - "description": "is follower", - "type": "boolean" + "status": { + "type": "string" }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "username": { "type": "string" + } + } + }, + "schema.SearchResp": { + "type": "object", + "properties": { + "count": { + "type": "integer" }, - "member_actions": { - "description": "MemberActions", + "list": { + "description": "search response", "type": "array", "items": { - "$ref": "#/definitions/schema.PermissionMemberAction" + "$ref": "#/definitions/schema.SearchResult" } + } + } + }, + "schema.SearchResult": { + "type": "object", + "properties": { + "object": { + "description": "this object", + "allOf": [ + { + "$ref": "#/definitions/schema.SearchObject" + } + ] }, - "original_text": { - "description": "original text", + "object_type": { + "description": "object_type", "type": "string" - }, - "parsed_text": { - "description": "parsed text", + } + } + }, + "schema.SendUserActivationReq": { + "type": "object", + "required": [ + "user_id" + ], + "properties": { + "user_id": { "type": "string" + } + } + }, + "schema.SiteBrandingReq": { + "type": "object", + "properties": { + "favicon": { + "type": "string", + "maxLength": 512 }, - "question_count": { - "description": "question amount", - "type": "integer" - }, - "slug_name": { - "description": "slug name", - "type": "string" + "logo": { + "type": "string", + "maxLength": 512 }, - "tag_id": { - "description": "tag id", - "type": "string" + "mobile_logo": { + "type": "string", + "maxLength": 512 }, - "updated_at": { - "description": "updated time", - "type": "integer" + "square_icon": { + "type": "string", + "maxLength": 512 } } }, - "schema.GetTagSynonymsResp": { + "schema.SiteBrandingResp": { "type": "object", "properties": { - "display_name": { - "description": "display name", - "type": "string" + "favicon": { + "type": "string", + "maxLength": 512 }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", - "type": "string" + "logo": { + "type": "string", + "maxLength": 512 }, - "slug_name": { - "description": "slug name", - "type": "string" + "mobile_logo": { + "type": "string", + "maxLength": 512 }, - "tag_id": { - "description": "tag id", - "type": "string" + "square_icon": { + "type": "string", + "maxLength": 512 } } }, - "schema.GetUserPageResp": { + "schema.SiteCustomCssHTMLReq": { "type": "object", "properties": { - "avatar": { - "description": "avatar", - "type": "string" - }, - "created_at": { - "description": "create time", - "type": "integer" + "custom_css": { + "type": "string", + "maxLength": 65536 }, - "deleted_at": { - "description": "delete time", - "type": "integer" + "custom_footer": { + "type": "string", + "maxLength": 65536 }, - "display_name": { - "description": "display name", - "type": "string" + "custom_head": { + "type": "string", + "maxLength": 65536 }, - "e_mail": { - "description": "email", - "type": "string" + "custom_header": { + "type": "string", + "maxLength": 65536 }, - "rank": { - "description": "rank", - "type": "integer" + "custom_sidebar": { + "type": "string", + "maxLength": 65536 + } + } + }, + "schema.SiteCustomCssHTMLResp": { + "type": "object", + "properties": { + "custom_css": { + "type": "string", + "maxLength": 65536 }, - "status": { - "description": "user status(normal,suspended,deleted,inactive)", - "type": "string" + "custom_footer": { + "type": "string", + "maxLength": 65536 }, - "suspended_at": { - "description": "suspended time", - "type": "integer" + "custom_head": { + "type": "string", + "maxLength": 65536 }, - "user_id": { - "description": "user id", - "type": "string" + "custom_header": { + "type": "string", + "maxLength": 65536 }, - "username": { - "description": "username", - "type": "string" + "custom_sidebar": { + "type": "string", + "maxLength": 65536 } } }, - "schema.GetUserResp": { + "schema.SiteGeneralReq": { "type": "object", + "required": [ + "contact_email", + "name", + "site_url" + ], "properties": { - "access_token": { - "description": "access token", - "type": "string" - }, - "answer_count": { - "description": "answer count", - "type": "integer" + "check_update": { + "type": "boolean" }, - "authority_group": { - "description": "authority group", - "type": "integer" + "contact_email": { + "type": "string", + "maxLength": 512 }, - "avatar": { - "description": "avatar", - "type": "string" + "description": { + "type": "string", + "maxLength": 2000 }, - "bio": { - "description": "bio markdown", - "type": "string" + "name": { + "type": "string", + "maxLength": 128 }, - "bio_html": { - "description": "bio html", - "type": "string" + "short_description": { + "type": "string", + "maxLength": 255 }, - "created_at": { - "description": "create time", - "type": "integer" + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "schema.SiteGeneralResp": { + "type": "object", + "required": [ + "contact_email", + "name", + "site_url" + ], + "properties": { + "check_update": { + "type": "boolean" }, - "display_name": { - "description": "display name", - "type": "string" + "contact_email": { + "type": "string", + "maxLength": 512 }, - "e_mail": { - "description": "email", - "type": "string" + "description": { + "type": "string", + "maxLength": 2000 }, - "follow_count": { - "description": "follow count", - "type": "integer" + "name": { + "type": "string", + "maxLength": 128 }, - "id": { - "description": "user id", - "type": "string" + "short_description": { + "type": "string", + "maxLength": 255 }, - "ip_info": { - "description": "ip info", - "type": "string" + "site_url": { + "type": "string", + "maxLength": 512 + } + } + }, + "schema.SiteInfoResp": { + "type": "object", + "properties": { + "branding": { + "$ref": "#/definitions/schema.SiteBrandingResp" }, - "is_admin": { - "description": "is admin", - "type": "boolean" + "custom_css_html": { + "$ref": "#/definitions/schema.SiteCustomCssHTMLResp" }, - "last_login_date": { - "description": "last login date", - "type": "integer" + "general": { + "$ref": "#/definitions/schema.SiteGeneralResp" }, - "location": { - "description": "location", - "type": "string" + "interface": { + "$ref": "#/definitions/schema.SiteInterfaceResp" }, - "mail_status": { - "description": "mail status(1 pass 2 to be verified)", - "type": "integer" + "login": { + "$ref": "#/definitions/schema.SiteLoginResp" }, - "mobile": { - "description": "mobile", + "revision": { "type": "string" }, - "notice_status": { - "description": "notice status(1 on 2off)", - "type": "integer" + "site_legal": { + "$ref": "#/definitions/schema.SiteLegalSimpleResp" }, - "question_count": { - "description": "question count", - "type": "integer" + "site_seo": { + "$ref": "#/definitions/schema.SiteSeoResp" }, - "rank": { - "description": "rank", - "type": "integer" + "site_users": { + "$ref": "#/definitions/schema.SiteUsersResp" }, - "status": { - "description": "user status", - "type": "string" + "site_write": { + "$ref": "#/definitions/schema.SiteWriteResp" }, - "username": { - "description": "username", - "type": "string" + "theme": { + "$ref": "#/definitions/schema.SiteThemeResp" }, - "website": { - "description": "website", + "version": { "type": "string" } } }, - "schema.GetVoteWithPageResp": { + "schema.SiteInterfaceReq": { "type": "object", + "required": [ + "default_avatar", + "language", + "time_zone" + ], "properties": { - "answer_id": { - "description": "answer id", - "type": "string" - }, - "content": { - "description": "content", - "type": "string" - }, - "created_at": { - "description": "create time", - "type": "integer" - }, - "object_id": { - "description": "object id", - "type": "string" - }, - "object_type": { - "description": "object type", + "default_avatar": { "type": "string", "enum": [ - "question", - "answer", - "tag", - "comment" + "system", + "gravatar" ] }, - "question_id": { - "description": "question id", + "gravatar_base_url": { "type": "string" }, - "title": { - "description": "title", - "type": "string" + "language": { + "type": "string", + "maxLength": 128 }, - "vote_type": { - "description": "vote type", - "type": "string" + "time_zone": { + "type": "string", + "maxLength": 128 } } }, - "schema.NotificationClearIDRequest": { + "schema.SiteInterfaceResp": { "type": "object", + "required": [ + "default_avatar", + "language", + "time_zone" + ], "properties": { - "id": { + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" + }, + "language": { + "type": "string", + "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, - "schema.NotificationClearRequest": { + "schema.SiteLegalReq": { "type": "object", + "required": [ + "external_content_display" + ], "properties": { - "type": { - "description": "inbox achievement", + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "privacy_policy_original_text": { + "type": "string" + }, + "privacy_policy_parsed_text": { + "type": "string" + }, + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { "type": "string" } } }, - "schema.PermissionMemberAction": { + "schema.SiteLegalResp": { "type": "object", + "required": [ + "external_content_display" + ], "properties": { - "action": { + "external_content_display": { + "type": "string", + "enum": [ + "always_display", + "ask_before_display" + ] + }, + "privacy_policy_original_text": { "type": "string" }, - "name": { + "privacy_policy_parsed_text": { "type": "string" }, - "type": { + "terms_of_service_original_text": { + "type": "string" + }, + "terms_of_service_parsed_text": { "type": "string" } } }, - "schema.QuestionAdd": { + "schema.SiteLegalSimpleResp": { "type": "object", "required": [ - "content", - "html", - "tags", - "title" + "external_content_display" ], "properties": { - "content": { - "description": "content", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "html": { - "description": "html", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "tags": { - "description": "tags", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } - }, - "title": { - "description": "question title", + "external_content_display": { "type": "string", - "maxLength": 150, - "minLength": 6 + "enum": [ + "always_display", + "ask_before_display" + ] } } }, - "schema.QuestionSearch": { + "schema.SiteLoginReq": { "type": "object", "properties": { - "order": { - "description": "Search order by", - "type": "string" + "allow_email_domains": { + "type": "array", + "items": { + "type": "string" + } }, - "page": { - "description": "Query number of pages", - "type": "integer" + "allow_email_registrations": { + "type": "boolean" }, - "page_size": { - "description": "Search page size", - "type": "integer" + "allow_new_registrations": { + "type": "boolean" }, - "tags": { - "description": "Search tag", + "allow_password_login": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" + } + } + }, + "schema.SiteLoginResp": { + "type": "object", + "properties": { + "allow_email_domains": { "type": "array", "items": { "type": "string" } }, - "username": { - "description": "Search username", - "type": "string" + "allow_email_registrations": { + "type": "boolean" + }, + "allow_new_registrations": { + "type": "boolean" + }, + "allow_password_login": { + "type": "boolean" + }, + "login_required": { + "type": "boolean" } } }, - "schema.QuestionUpdate": { + "schema.SiteSeoReq": { "type": "object", "required": [ - "content", - "html", - "id", - "tags", - "title" + "permalink", + "robots" ], "properties": { - "content": { - "description": "content", - "type": "string", - "maxLength": 65535, - "minLength": 6 - }, - "edit_summary": { - "description": "edit summary", - "type": "string" - }, - "html": { - "description": "html", - "type": "string", - "maxLength": 65535, - "minLength": 6 + "permalink": { + "type": "integer", + "maximum": 4, + "minimum": 0 }, - "id": { - "description": "question id", + "robots": { "type": "string" - }, - "tags": { - "description": "tags", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } - }, - "title": { - "description": "question title", - "type": "string", - "maxLength": 150, - "minLength": 6 } } }, - "schema.RemoveAnswerReq": { + "schema.SiteSeoResp": { "type": "object", "required": [ - "id" + "permalink", + "robots" ], "properties": { - "id": { - "description": "answer id", + "permalink": { + "type": "integer", + "maximum": 4, + "minimum": 0 + }, + "robots": { "type": "string" } } }, - "schema.RemoveCommentReq": { + "schema.SiteThemeReq": { "type": "object", "required": [ - "comment_id" + "theme" ], "properties": { - "comment_id": { - "description": "comment id", - "type": "string" + "color_scheme": { + "type": "string", + "maxLength": 100 + }, + "theme": { + "type": "string", + "maxLength": 255 + }, + "theme_config": { + "type": "object", + "additionalProperties": true } } }, - "schema.RemoveQuestionReq": { + "schema.SiteThemeResp": { "type": "object", - "required": [ - "id" - ], "properties": { - "id": { - "description": "question id", + "color_scheme": { + "type": "string" + }, + "theme": { "type": "string" + }, + "theme_config": { + "type": "object", + "additionalProperties": true + }, + "theme_options": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.ThemeOption" + } } } }, - "schema.RemoveTagReq": { + "schema.SiteUsersReq": { "type": "object", "required": [ - "tag_id" + "default_avatar" ], "properties": { - "tag_id": { - "description": "tag_id", + "allow_update_avatar": { + "type": "boolean" + }, + "allow_update_bio": { + "type": "boolean" + }, + "allow_update_display_name": { + "type": "boolean" + }, + "allow_update_location": { + "type": "boolean" + }, + "allow_update_username": { + "type": "boolean" + }, + "allow_update_website": { + "type": "boolean" + }, + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" } } }, - "schema.ReportHandleReq": { + "schema.SiteUsersResp": { "type": "object", "required": [ - "flagged_type", - "id" + "default_avatar" ], "properties": { - "flagged_content": { - "type": "string" + "allow_update_avatar": { + "type": "boolean" }, - "flagged_type": { - "type": "integer" + "allow_update_bio": { + "type": "boolean" }, - "id": { + "allow_update_display_name": { + "type": "boolean" + }, + "allow_update_location": { + "type": "boolean" + }, + "allow_update_username": { + "type": "boolean" + }, + "allow_update_website": { + "type": "boolean" + }, + "default_avatar": { + "type": "string", + "enum": [ + "system", + "gravatar" + ] + }, + "gravatar_base_url": { "type": "string" } } }, - "schema.SearchListResp": { + "schema.SiteWriteReq": { "type": "object", "properties": { - "count": { + "authorized_attachment_extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorized_image_extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "max_attachment_size": { + "type": "integer" + }, + "max_image_megapixel": { "type": "integer" }, - "extra": { - "description": "extra fields" + "max_image_size": { + "type": "integer" }, - "list": { - "description": "search response", + "recommend_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.SiteWriteTag" + } + }, + "required_tag": { + "type": "boolean" + }, + "reserved_tags": { "type": "array", "items": { - "$ref": "#/definitions/schema.SearchResp" + "$ref": "#/definitions/schema.SiteWriteTag" } + }, + "restrict_answer": { + "type": "boolean" } } }, - "schema.SearchObject": { + "schema.SiteWriteResp": { "type": "object", "properties": { - "accepted": { - "type": "boolean" + "authorized_attachment_extensions": { + "type": "array", + "items": { + "type": "string" + } }, - "answer_count": { - "type": "integer" + "authorized_image_extensions": { + "type": "array", + "items": { + "type": "string" + } }, - "created_at": { + "max_attachment_size": { "type": "integer" }, - "excerpt": { - "type": "string" - }, - "id": { - "type": "string" + "max_image_megapixel": { + "type": "integer" }, - "status": { - "description": "Status", - "type": "string" + "max_image_size": { + "type": "integer" }, - "tags": { - "description": "tags", + "recommend_tags": { "type": "array", "items": { - "$ref": "#/definitions/schema.TagResp" + "$ref": "#/definitions/schema.SiteWriteTag" } }, - "title": { - "type": "string" + "required_tag": { + "type": "boolean" }, - "user_info": { - "description": "user info", - "$ref": "#/definitions/schema.UserBasicInfo" + "reserved_tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.SiteWriteTag" + } }, - "vote_count": { - "type": "integer" + "restrict_answer": { + "type": "boolean" } } }, - "schema.SearchResp": { + "schema.SiteWriteTag": { "type": "object", + "required": [ + "slug_name" + ], "properties": { - "object": { - "description": "this object", - "$ref": "#/definitions/schema.SearchObject" + "display_name": { + "type": "string" }, - "object_type": { - "description": "object_type", + "slug_name": { "type": "string" } } }, - "schema.SiteGeneralReq": { + "schema.TagItem": { "type": "object", - "required": [ - "description", - "name", - "short_description" - ], "properties": { - "description": { + "display_name": { + "description": "display_name", "type": "string", - "maxLength": 2000 + "maxLength": 35 }, - "name": { - "type": "string", - "maxLength": 128 + "original_text": { + "description": "original text", + "type": "string" }, - "short_description": { + "slug_name": { + "description": "slug_name", "type": "string", - "maxLength": 255 + "maxLength": 35 } } }, - "schema.SiteGeneralResp": { + "schema.TagResp": { "type": "object", - "required": [ - "description", - "name", - "short_description" - ], "properties": { - "description": { - "type": "string", - "maxLength": 2000 + "display_name": { + "type": "string" }, - "name": { - "type": "string", - "maxLength": 128 + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" }, - "short_description": { - "type": "string", - "maxLength": 255 + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" } } }, - "schema.SiteInterfaceReq": { + "schema.TagSynonym": { + "type": "object", + "properties": { + "display_name": { + "description": "display name", + "type": "string" + }, + "main_tag_slug_name": { + "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "type": "string" + }, + "slug_name": { + "description": "slug name", + "type": "string" + }, + "tag_id": { + "description": "tag id", + "type": "string" + } + } + }, + "schema.ThemeOption": { "type": "object", - "required": [ - "language", - "theme" - ], "properties": { - "language": { - "type": "string", - "maxLength": 128 - }, - "logo": { - "type": "string", - "maxLength": 256 + "label": { + "type": "string" }, - "theme": { - "type": "string", - "maxLength": 128 + "value": { + "type": "string" } } }, - "schema.SiteInterfaceResp": { + "schema.UIOptionAction": { "type": "object", - "required": [ - "language", - "theme" - ], "properties": { - "language": { - "type": "string", - "maxLength": 128 + "loading": { + "$ref": "#/definitions/schema.LoadingAction" }, - "logo": { - "type": "string", - "maxLength": 256 + "method": { + "type": "string" }, - "theme": { - "type": "string", - "maxLength": 128 + "on_complete": { + "$ref": "#/definitions/schema.OnCompleteAction" + }, + "url": { + "type": "string" } } }, - "schema.TagItem": { + "schema.UnreviewedRevisionInfoInfo": { "type": "object", "properties": { - "display_name": { - "description": "display_name", - "type": "string", - "maxLength": 35 + "answer_accepted": { + "type": "boolean" }, - "original_text": { - "description": "original text", + "answer_count": { + "type": "integer" + }, + "answer_id": { "type": "string" }, - "parsed_text": { - "description": "parsed text", + "comment_id": { "type": "string" }, - "slug_name": { - "description": "slug_name", - "type": "string", - "maxLength": 35 + "content": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "html": { + "type": "string" + }, + "object_creator_user_id": { + "type": "string" + }, + "object_id": { + "type": "string" + }, + "object_type": { + "type": "string" + }, + "question_id": { + "type": "string" + }, + "show_status": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagResp" + } + }, + "title": { + "type": "string" + }, + "url_title": { + "type": "string" } } }, - "schema.TagResp": { + "schema.UpdateBadgeStatusReq": { "type": "object", + "required": [ + "id", + "status" + ], "properties": { - "display_name": { - "type": "string" - }, - "main_tag_slug_name": { - "description": "if main tag slug name is not empty, this tag is synonymous with the main tag", + "id": { + "description": "badge id", "type": "string" }, - "slug_name": { - "type": "string" + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] } } }, "schema.UpdateCommentReq": { "type": "object", "required": [ - "comment_id" + "comment_id", + "original_text" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "description": "whether user can delete it", + "type": "string" + }, "comment_id": { "description": "comment id", "type": "string" }, "original_text": { "description": "original comment content", - "type": "string" - }, - "parsed_text": { - "description": "parsed comment content", - "type": "string" + "type": "string", + "maxLength": 600, + "minLength": 2 } } }, @@ -5303,55 +11304,143 @@ }, "schema.UpdateInfoRequest": { "type": "object", - "required": [ - "display_name" - ], "properties": { "avatar": { - "description": "avatar", - "type": "string", - "maxLength": 500 + "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { - "description": "bio", - "type": "string", - "maxLength": 4096 - }, - "bio_html": { - "description": "bio", "type": "string", "maxLength": 4096 }, "display_name": { - "description": "display_name", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "location": { - "description": "location", "type": "string", "maxLength": 100 }, "username": { - "description": "username", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "website": { - "description": "website", "type": "string", "maxLength": 500 } } }, + "schema.UpdatePluginConfigReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "config_fields": { + "type": "object", + "additionalProperties": {} + }, + "plugin_slug_name": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdatePluginStatusReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "plugin_slug_name": { + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdatePrivilegesConfigReq": { + "type": "object", + "required": [ + "level" + ], + "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, + "level": { + "minimum": 1, + "allOf": [ + { + "$ref": "#/definitions/schema.PrivilegeLevel" + } + ] + } + } + }, + "schema.UpdateReactionReq": { + "type": "object", + "required": [ + "emoji", + "object_id", + "reaction" + ], + "properties": { + "emoji": { + "type": "string", + "enum": [ + "heart", + "smile", + "frown" + ] + }, + "object_id": { + "type": "string" + }, + "reaction": { + "type": "string", + "enum": [ + "activate", + "deactivate" + ] + } + } + }, + "schema.UpdateReviewReq": { + "type": "object", + "required": [ + "review_id", + "status" + ], + "properties": { + "review_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "approve", + "reject" + ] + } + } + }, "schema.UpdateSMTPConfigReq": { "type": "object", "properties": { "encryption": { - "description": "\"\" SSL", + "description": "\"\" SSL TLS", "type": "string", "enum": [ - "SSL" + "SSL", + "TLS" ] }, "from_email": { @@ -5406,37 +11495,116 @@ "description": "original text", "type": "string" }, - "parsed_text": { - "description": "parsed text", + "slug_name": { + "description": "slug_name", + "type": "string", + "maxLength": 35 + }, + "tag_id": { + "description": "tag_id", + "type": "string" + } + } + }, + "schema.UpdateTagSynonymReq": { + "type": "object", + "required": [ + "synonym_tag_list", + "tag_id" + ], + "properties": { + "synonym_tag_list": { + "description": "synonym tag list", + "type": "array", + "items": { + "$ref": "#/definitions/schema.TagItem" + } + }, + "tag_id": { + "description": "tag_id", + "type": "string" + } + } + }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "color_scheme", + "language" + ], + "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, + "schema.UpdateUserNotificationConfigReq": { + "type": "object", + "properties": { + "all_new_question": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "all_new_question_for_following_tags": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + }, + "inbox": { + "$ref": "#/definitions/schema.NotificationChannelConfig" + } + } + }, + "schema.UpdateUserPasswordReq": { + "type": "object", + "required": [ + "password", + "user_id" + ], + "properties": { + "password": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "user_id": { "type": "string" + } + } + }, + "schema.UpdateUserPluginConfigReq": { + "type": "object", + "required": [ + "plugin_slug_name" + ], + "properties": { + "config_fields": { + "type": "object", + "additionalProperties": {} }, - "slug_name": { - "description": "slug_name", + "plugin_slug_name": { "type": "string", - "maxLength": 35 - }, - "tag_id": { - "description": "tag_id", - "type": "string" + "maxLength": 100 } } }, - "schema.UpdateTagSynonymReq": { + "schema.UpdateUserRoleReq": { "type": "object", "required": [ - "synonym_tag_list", - "tag_id" + "role_id", + "user_id" ], "properties": { - "synonym_tag_list": { - "description": "synonym tag list", - "type": "array", - "items": { - "$ref": "#/definitions/schema.TagItem" - } + "role_id": { + "description": "role id", + "type": "integer" }, - "tag_id": { - "description": "tag_id", + "user_id": { + "description": "user id", "type": "string" } } @@ -5448,8 +11616,10 @@ "user_id" ], "properties": { + "remove_all_content": { + "type": "boolean" + }, "status": { - "description": "user status", "type": "string", "enum": [ "normal", @@ -5458,8 +11628,23 @@ "inactive" ] }, + "suspend_duration": { + "type": "string", + "enum": [ + "24h", + "48h", + "72h", + "7d", + "14d", + "1m", + "2m", + "3m", + "6m", + "1y", + "forever" + ] + }, "user_id": { - "description": "user id", "type": "string" } } @@ -5468,35 +11653,33 @@ "type": "object", "properties": { "avatar": { - "description": "avatar", "type": "string" }, "display_name": { - "description": "display_name", "type": "string" }, - "ip_info": { - "description": "ip info", + "id": { + "type": "string" + }, + "language": { "type": "string" }, "location": { - "description": "location", "type": "string" }, "rank": { - "description": "rank", "type": "integer" }, "status": { - "description": "status", "type": "string" }, + "suspended_until": { + "type": "integer" + }, "username": { - "description": "name", "type": "string" }, "website": { - "description": "website", "type": "string" } } @@ -5507,9 +11690,20 @@ "e_mail" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "e_mail": { "type": "string", "maxLength": 500 + }, + "pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 } } }, @@ -5525,53 +11719,212 @@ } } }, - "schema.UserEmailLogin": { + "schema.UserEmailLoginReq": { "type": "object", + "required": [ + "e_mail", + "pass" + ], "properties": { "captcha_code": { - "description": "captcha_code", "type": "string" }, "captcha_id": { - "description": "captcha_id", "type": "string" }, "e_mail": { - "description": "e_mail", - "type": "string" + "type": "string", + "maxLength": 500 }, "pass": { - "description": "password", + "type": "string", + "maxLength": 32, + "minLength": 8 + } + } + }, + "schema.UserLoginResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "description": "avatar", + "type": "string" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "have_password": { + "description": "user have password", + "type": "boolean" + }, + "id": { + "description": "user id", + "type": "string" + }, + "language": { + "description": "language", + "type": "string" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "role_id": { + "description": "role id", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "suspended_until": { + "description": "suspended until timestamp", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "visit_token": { + "description": "visit token", + "type": "string" + }, + "website": { + "description": "website", "type": "string" } } }, - "schema.UserModifyPassWordRequest": { + "schema.UserModifyPasswordReq": { "type": "object", + "required": [ + "pass" + ], "properties": { - "old_pass": { - "description": "old password", + "captcha_code": { "type": "string" }, - "pass": { - "description": "password", + "captcha_id": { "type": "string" + }, + "old_pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 + }, + "pass": { + "type": "string", + "maxLength": 32, + "minLength": 8 } } }, - "schema.UserNoticeSetRequest": { + "schema.UserRankingResp": { "type": "object", "properties": { - "notice_switch": { - "type": "boolean" + "staffs": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_reputation": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } + }, + "users_with_the_most_vote": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.UserRankingSimpleInfo" + } } } }, - "schema.UserNoticeSetResp": { + "schema.UserRankingSimpleInfo": { "type": "object", "properties": { - "notice_switch": { - "type": "boolean" + "avatar": { + "description": "avatar", + "type": "string" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "username": { + "description": "username", + "type": "string" + }, + "vote_count": { + "description": "vote", + "type": "integer" } } }, @@ -5583,12 +11936,10 @@ ], "properties": { "code": { - "description": "code", "type": "string", "maxLength": 100 }, "pass": { - "description": "Password", "type": "string", "maxLength": 32 } @@ -5602,18 +11953,22 @@ "pass" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "e_mail": { - "description": "email", "type": "string", "maxLength": 500 }, "name": { - "description": "name", "type": "string", - "maxLength": 30 + "maxLength": 30, + "minLength": 2 }, "pass": { - "description": "password", "type": "string", "maxLength": 32, "minLength": 8 @@ -5627,15 +11982,24 @@ ], "properties": { "captcha_code": { - "description": "captcha_code", "type": "string" }, "captcha_id": { - "description": "captcha_id", "type": "string" }, "e_mail": { - "description": "e_mail", + "type": "string", + "maxLength": 500 + } + } + }, + "schema.UserUnsubscribeNotificationReq": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { "type": "string", "maxLength": 500 } @@ -5647,12 +12011,16 @@ "object_id" ], "properties": { + "captcha_code": { + "type": "string" + }, + "captcha_id": { + "type": "string" + }, "is_cancel": { - "description": "is cancel", "type": "boolean" }, "object_id": { - "description": "id", "type": "string" } } @@ -5673,6 +12041,21 @@ "type": "integer" } } + }, + "translator.LangOption": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "progress": { + "description": "Translation completion percentage", + "type": "integer" + }, + "value": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cf6bcd49b..9cdf248e1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,11 +1,48 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +basePath: / definitions: - entity.AdminSetAnswerStatusRequest: + constant.NotificationChannelKey: + enum: + - email + type: string + x-enum-varnames: + - EmailChannel + constant.Privilege: properties: - answer_id: + key: type: string - status: + label: type: string + value: + minimum: 1 + type: integer type: object + entity.BadgeLevel: + enum: + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - BadgeLevelBronze + - BadgeLevelSilver + - BadgeLevelGold handler.RespBody: properties: code: @@ -20,12 +57,156 @@ definitions: description: reason key type: string type: object + install.CheckConfigFileResp: + properties: + config_file_exist: + type: boolean + db_connection_success: + type: boolean + db_table_exist: + type: boolean + type: object + install.CheckDatabaseReq: + properties: + db_file: + type: string + db_host: + type: string + db_name: + type: string + db_password: + type: string + db_type: + enum: + - postgres + - sqlite3 + - mysql + type: string + db_username: + type: string + ssl_cert: + type: string + ssl_enabled: + type: boolean + ssl_key: + type: string + ssl_mode: + type: string + ssl_root_cert: + type: string + required: + - db_type + type: object + install.InitBaseInfoReq: + properties: + contact_email: + maxLength: 500 + type: string + email: + maxLength: 500 + type: string + external_content_display: + enum: + - always_display + - ask_before_display + type: string + lang: + maxLength: 30 + type: string + login_required: + type: boolean + name: + maxLength: 30 + minLength: 2 + type: string + password: + maxLength: 32 + minLength: 8 + type: string + site_name: + maxLength: 30 + type: string + site_url: + maxLength: 512 + type: string + required: + - contact_email + - email + - external_content_display + - lang + - name + - password + - site_name + - site_url + type: object pager.PageModel: properties: count: type: integer list: {} type: object + plugin.EmbedConfig: + properties: + enable: + type: boolean + platform: + type: string + type: object + plugin.RenderConfig: + properties: + select_theme: + type: string + type: object + schema.AcceptAnswerReq: + properties: + answer_id: + type: string + question_id: + maxLength: 30 + type: string + required: + - question_id + type: object + schema.ActObjectInfo: + properties: + answer_id: + type: string + display_name: + type: string + main_tag_slug_name: + type: string + object_type: + type: string + question_id: + type: string + title: + type: string + username: + type: string + type: object + schema.ActObjectTimeline: + properties: + activity_id: + type: string + activity_type: + type: string + cancelled: + type: boolean + cancelled_at: + type: integer + comment: + type: string + created_at: + type: integer + object_id: + type: string + object_type: + type: string + revision_id: + type: string + user_info: + $ref: '#/definitions/schema.UserBasicInfo' + type: object schema.ActionRecordResp: properties: captcha_id: @@ -37,6 +218,10 @@ definitions: type: object schema.AddCommentReq: properties: + captcha_code: + type: string + captcha_id: + type: string mention_username_list: description: '@ user id list' items: @@ -47,9 +232,8 @@ definitions: type: string original_text: description: original comment content - type: string - parsed_text: - description: parsed comment content + maxLength: 600 + minLength: 2 type: string reply_comment_id: description: reply comment id @@ -57,10 +241,14 @@ definitions: required: - object_id - original_text - - parsed_text type: object schema.AddReportReq: properties: + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string content: description: report content maxLength: 500 @@ -76,69 +264,190 @@ definitions: - object_id - report_type type: object - schema.AdminSetQuestionStatusRequest: + schema.AddTagReq: properties: - question_id: + display_name: + description: display_name + maxLength: 35 type: string - status: + original_text: + description: original text + maxLength: 65536 type: string + slug_name: + description: slug_name + maxLength: 35 + type: string + required: + - display_name + - original_text + - slug_name type: object - schema.AnswerAddReq: + schema.AddUserReq: properties: - content: - description: content + display_name: + maxLength: 30 + minLength: 2 type: string - html: - description: html + email: + maxLength: 500 type: string - question_id: - description: question_id + password: + maxLength: 32 + minLength: 8 + type: string + required: + - display_name + - email + - password + type: object + schema.AddUsersReq: + properties: + users: + description: users info line by line type: string type: object - schema.AnswerAdoptedReq: + schema.AdminUpdateAnswerStatusReq: properties: answer_id: type: string + status: + enum: + - available + - deleted + type: string + required: + - answer_id + - status + type: object + schema.AdminUpdateQuestionStatusReq: + properties: question_id: - description: question_id type: string + status: + enum: + - available + - closed + - deleted + type: string + required: + - question_id + - status type: object - schema.AnswerList: + schema.AnswerAddReq: properties: - order: - description: 1 Default 2 time + captcha_code: type: string - page: - description: Query number of pages + captcha_id: + type: string + content: + maxLength: 65535 + minLength: 6 + type: string + question_id: + type: string + required: + - content + type: object + schema.AnswerInfo: + properties: + accepted: type: integer - page_size: - description: Search page size + collected: + type: boolean + content: + type: string + create_time: type: integer + html: + type: string + id: + type: string + member_actions: + description: MemberActions + items: + $ref: '#/definitions/schema.PermissionMemberAction' + type: array question_id: - description: question_id + type: string + question_info: + $ref: '#/definitions/schema.QuestionInfoResp' + status: + type: integer + update_time: + type: integer + update_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + user_info: + $ref: '#/definitions/schema.UserBasicInfo' + vote_count: + type: integer + vote_status: type: string type: object schema.AnswerUpdateReq: properties: + captcha_code: + type: string + captcha_id: + type: string content: - description: content + maxLength: 65535 + minLength: 6 type: string edit_summary: - description: edit_summary - type: string - html: - description: html type: string id: - description: id type: string question_id: - description: question_id type: string title: - description: title + type: string + required: + - content + type: object + schema.AvatarInfo: + properties: + custom: + maxLength: 200 + type: string + gravatar: + maxLength: 200 + type: string + type: + maxLength: 100 + type: string + type: object + schema.BadgeListInfo: + properties: + award_count: + description: badge award count + type: integer + earned_count: + description: badge earned count + type: integer + icon: + description: badge icon + type: string + id: + description: badge id + type: string + level: + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level + name: + description: badge name type: string type: object + schema.BadgeStatus: + enum: + - active + - inactive + type: string + x-enum-varnames: + - BadgeStatusActive + - BadgeStatusInactive schema.CloseQuestionReq: properties: close_msg: @@ -154,11 +463,11 @@ definitions: type: object schema.CollectionSwitchReq: properties: + bookmark: + type: boolean group_id: - description: user collection group TagID type: string object_id: - description: object TagID type: string required: - group_id @@ -167,33 +476,240 @@ definitions: schema.CollectionSwitchResp: properties: object_collection_count: - type: string - object_id: - type: string - switch: - type: boolean + type: integer type: object - schema.FollowReq: + schema.ConfigField: properties: - is_cancel: - description: is cancel + description: + type: string + name: + type: string + options: + items: + $ref: '#/definitions/schema.ConfigFieldOption' + type: array + required: type: boolean - object_id: - description: object id + title: type: string - required: - - object_id + type: + type: string + ui_options: + $ref: '#/definitions/schema.ConfigFieldUIOptions' + value: {} type: object - schema.FollowResp: + schema.ConfigFieldOption: properties: - follows: - description: the followers of object - type: integer - is_followed: - description: if user is followed object will be true,otherwise false - type: boolean + label: + type: string + value: + type: string type: object - schema.GetCommentPersonalWithPageResp: + schema.ConfigFieldUIOptions: + properties: + action: + $ref: '#/definitions/schema.UIOptionAction' + class_name: + type: string + field_class_name: + type: string + input_type: + type: string + label: + type: string + placeholder: + type: string + rows: + type: string + text: + type: string + variant: + type: string + type: object + schema.ConnectorInfoResp: + properties: + icon: + type: string + link: + type: string + name: + type: string + type: object + schema.ConnectorUserInfoResp: + properties: + binding: + type: boolean + external_id: + type: string + icon: + type: string + link: + type: string + name: + type: string + type: object + schema.DeletePermanentlyReq: + properties: + type: + enum: + - users + - questions + - answers + type: string + required: + - type + type: object + schema.EditUserProfileReq: + properties: + display_name: + maxLength: 30 + minLength: 2 + type: string + email: + maxLength: 500 + type: string + user_id: + type: string + username: + maxLength: 30 + minLength: 2 + type: string + required: + - display_name + - email + - user_id + type: object + schema.ExternalLoginBindingUserSendEmailReq: + properties: + binding_key: + maxLength: 100 + type: string + email: + maxLength: 512 + type: string + must: + description: |- + If must is true, whatever email if exists, try to bind user. + If must is false, when email exist, will only be prompted with a warning. + type: boolean + required: + - binding_key + - email + type: object + schema.ExternalLoginBindingUserSendEmailResp: + properties: + access_token: + type: string + email_exist_and_must_be_confirmed: + type: boolean + type: object + schema.ExternalLoginUnbindingReq: + properties: + external_id: + maxLength: 128 + type: string + required: + - external_id + type: object + schema.FollowReq: + properties: + is_cancel: + description: is cancel + type: boolean + object_id: + description: object id + type: string + required: + - object_id + type: object + schema.FollowResp: + properties: + follows: + description: the followers of object + type: integer + is_followed: + description: if user is followed object will be true,otherwise false + type: boolean + type: object + schema.GetAnswerInfoResp: + properties: + info: + $ref: '#/definitions/schema.AnswerInfo' + question: + $ref: '#/definitions/schema.QuestionInfoResp' + type: object + schema.GetBadgeInfoResp: + properties: + award_count: + description: badge award count + type: integer + description: + description: badge description + type: string + earned_count: + description: badge earned count + type: integer + icon: + description: badge icon + type: string + id: + description: badge id + type: string + is_single: + description: badge is single or multiple + type: boolean + level: + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level + name: + description: badge name + type: string + type: object + schema.GetBadgeListPagedResp: + properties: + award_count: + description: badge award count + type: integer + description: + description: badge description + type: string + earned: + description: badge earned count + type: boolean + group_name: + description: badge group name + type: string + icon: + description: badge icon + type: string + id: + description: badge id + type: string + level: + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level + name: + description: badge name + type: string + status: + allOf: + - $ref: '#/definitions/schema.BadgeStatus' + description: badge status + type: object + schema.GetBadgeListResp: + properties: + badges: + description: badge list info + items: + $ref: '#/definitions/schema.BadgeListInfo' + type: array + group_name: + description: badge group name + type: string + type: object + schema.GetCommentPersonalWithPageResp: properties: answer_id: description: answer id @@ -224,6 +740,9 @@ definitions: title: description: title type: string + url_title: + description: url title + type: string type: object schema.GetCommentResp: properties: @@ -284,6 +803,89 @@ definitions: description: user vote amount type: integer type: object + schema.GetCurrentLoginUserInfoResp: + properties: + access_token: + description: access token + type: string + answer_count: + description: answer count + type: integer + authority_group: + description: authority group + type: integer + avatar: + $ref: '#/definitions/schema.AvatarInfo' + bio: + description: bio markdown + type: string + bio_html: + description: bio html + type: string + color_scheme: + description: Color scheme + type: string + created_at: + description: create time + type: integer + display_name: + description: display name + type: string + e_mail: + description: email + type: string + follow_count: + description: follow count + type: integer + have_password: + description: user have password + type: boolean + id: + description: user id + type: string + language: + description: language + type: string + last_login_date: + description: last login date + type: integer + location: + description: location + type: string + mail_status: + description: mail status(1 pass 2 to be verified) + type: integer + mobile: + description: mobile + type: string + notice_status: + description: notice status(1 on 2off) + type: integer + question_count: + description: question count + type: integer + rank: + description: rank + type: integer + role_id: + description: role id + type: integer + status: + description: user status + type: string + suspended_until: + description: suspended until timestamp + type: integer + username: + description: username + type: string + visit_token: + description: visit token + type: string + website: + description: website + type: string + type: object schema.GetFollowingTagsResp: properties: display_name: @@ -293,6 +895,10 @@ definitions: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string + recommend: + type: boolean + reserved: + type: boolean slug_name: description: slug name type: string @@ -300,6 +906,15 @@ definitions: description: tag id type: string type: object + schema.GetObjectTimelineResp: + properties: + object_info: + $ref: '#/definitions/schema.ActObjectInfo' + timeline: + items: + $ref: '#/definitions/schema.ActObjectTimeline' + type: array + type: object schema.GetOtherUserInfoByUsernameResp: properties: answer_count: @@ -328,12 +943,6 @@ definitions: id: description: user id type: string - ip_info: - description: ip info - type: string - is_admin: - description: is admin - type: boolean last_login_date: description: last login date type: integer @@ -353,6 +962,9 @@ definitions: type: string status_msg: type: string + suspended_until: + description: suspended until timestamp + type: integer username: description: username type: string @@ -362,12 +974,51 @@ definitions: type: object schema.GetOtherUserInfoResp: properties: - has: - type: boolean info: $ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp' type: object - schema.GetRankPersonalWithPageResp: + schema.GetPluginConfigResp: + properties: + config_fields: + items: + $ref: '#/definitions/schema.ConfigField' + type: array + description: + type: string + name: + type: string + slug_name: + type: string + version: + type: string + type: object + schema.GetPluginListResp: + properties: + description: + type: string + enabled: + type: boolean + have_config: + type: boolean + link: + type: string + name: + type: string + slug_name: + type: string + version: + type: string + type: object + schema.GetPrivilegesConfigResp: + properties: + options: + items: + $ref: '#/definitions/schema.PrivilegeOption' + type: array + selected_level: + $ref: '#/definitions/schema.PrivilegeLevel' + type: object + schema.GetRankPersonalPageResp: properties: answer_id: description: answer id @@ -401,58 +1052,105 @@ definitions: title: description: title type: string + url_title: + description: url title + type: string type: object - schema.GetReportTypeResp: + schema.GetReportListPageResp: properties: - content_type: - description: content type - type: string - description: - description: report description - type: string - have_content: - description: is have content + answer_accepted: type: boolean - name: - description: report name + answer_count: + type: integer + answer_id: type: string - source: - description: report source + author_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + comment_id: type: string - type: - description: report type - type: integer - type: object - schema.GetRevisionResp: - properties: - content: - description: content parsed - create_at: + created_at: type: integer - id: - description: id + flag_id: type: string object_id: - description: object id - type: string - reason: type: string - status: - description: 'revision status(normal: 1; delete 2)' + object_show_status: type: integer - title: - description: title - type: string - use_id: - description: user id + object_status: + type: integer + object_type: + enum: + - question + - answer + - comment + type: string + original_text: + type: string + parsed_text: + type: string + question_id: + type: string + reason: + $ref: '#/definitions/schema.ReasonItem' + reason_content: + type: string + submit_at: + type: integer + submitter_user: + $ref: '#/definitions/schema.UserBasicInfo' + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + url_title: + type: string + type: object + schema.GetReviewingTypeResp: + properties: + label: + type: string + name: + type: string + todo_amount: + type: integer + type: object + schema.GetRevisionResp: + properties: + content: {} + create_at: + type: integer + id: + type: string + object_id: + type: string + reason: + type: string + status: + type: integer + title: + type: string + url_title: + type: string + use_id: type: string user_info: $ref: '#/definitions/schema.UserBasicInfo' type: object + schema.GetRoleResp: + properties: + description: + type: string + id: + type: integer + name: + type: string + type: object schema.GetSMTPConfigResp: properties: encryption: - description: '"" SSL' + description: '"" SSL TLS' type: string from_email: type: string @@ -469,11 +1167,38 @@ definitions: smtp_username: type: string type: object + schema.GetSiteLegalInfoResp: + properties: + privacy_policy_original_text: + type: string + privacy_policy_parsed_text: + type: string + terms_of_service_original_text: + type: string + terms_of_service_parsed_text: + type: string + type: object + schema.GetTagBasicResp: + properties: + display_name: + type: string + recommend: + type: boolean + reserved: + type: boolean + slug_name: + type: string + tag_id: + type: string + type: object schema.GetTagPageResp: properties: created_at: description: created time type: integer + description: + description: description + type: string display_name: description: display_name type: string @@ -495,6 +1220,10 @@ definitions: question_count: description: question amount type: integer + recommend: + type: boolean + reserved: + type: boolean slug_name: description: slug_name type: string @@ -508,63 +1237,143 @@ definitions: schema.GetTagResp: properties: created_at: - description: created time type: integer + description: + type: string display_name: - description: display name type: string excerpt: - description: excerpt type: string follow_count: - description: follower amount type: integer is_follower: - description: is follower type: boolean main_tag_slug_name: description: if main tag slug name is not empty, this tag is synonymous with the main tag type: string member_actions: - description: MemberActions items: $ref: '#/definitions/schema.PermissionMemberAction' type: array original_text: - description: original text type: string parsed_text: - description: parsed text type: string question_count: - description: question amount type: integer + recommend: + type: boolean + reserved: + type: boolean slug_name: - description: slug name + type: string + status: type: string tag_id: - description: tag id type: string updated_at: - description: updated time type: integer type: object schema.GetTagSynonymsResp: properties: - display_name: - description: display name + member_actions: + description: MemberActions + items: + $ref: '#/definitions/schema.PermissionMemberAction' + type: array + synonyms: + description: synonyms + items: + $ref: '#/definitions/schema.TagSynonym' + type: array + type: object + schema.GetUnreviewedPostPageResp: + properties: + answer_id: type: string - main_tag_slug_name: - description: if main tag slug name is not empty, this tag is synonymous with - the main tag + author_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + comment_id: type: string - slug_name: - description: slug name + created_at: + type: integer + object_id: type: string - tag_id: - description: tag id + object_show_status: + type: integer + object_status: + type: integer + object_type: + enum: + - question + - answer + - comment + type: string + original_text: + type: string + parsed_text: type: string + question_id: + type: string + reason: + type: string + review_id: + type: integer + submit_at: + type: integer + submitter_display_name: + type: string + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + url_title: + type: string + type: object + schema.GetUnreviewedRevisionResp: + properties: + info: + $ref: '#/definitions/schema.UnreviewedRevisionInfoInfo' + type: + type: string + unreviewed_info: + $ref: '#/definitions/schema.GetRevisionResp' + type: object + schema.GetUserActivationResp: + properties: + activation_url: + type: string + type: object + schema.GetUserBadgeAwardListResp: + properties: + earned_count: + description: badge award count + type: integer + icon: + description: badge icon + type: string + id: + description: badge id + type: string + level: + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level + name: + description: badge name + type: string + type: object + schema.GetUserNotificationConfigResp: + properties: + all_new_question: + $ref: '#/definitions/schema.NotificationChannelConfig' + all_new_question_for_following_tags: + $ref: '#/definitions/schema.NotificationChannelConfig' + inbox: + $ref: '#/definitions/schema.NotificationChannelConfig' type: object schema.GetUserPageResp: properties: @@ -586,12 +1395,21 @@ definitions: rank: description: rank type: integer + role_id: + description: role id + type: integer + role_name: + description: role name + type: string status: description: user status(normal,suspended,deleted,inactive) type: string suspended_at: description: suspended time type: integer + suspended_until: + description: suspended until time + type: integer user_id: description: user id type: string @@ -599,77 +1417,24 @@ definitions: description: username type: string type: object - schema.GetUserResp: + schema.GetUserPluginListResp: properties: - access_token: - description: access token + name: type: string - answer_count: - description: answer count - type: integer - authority_group: - description: authority group - type: integer + slug_name: + type: string + type: object + schema.GetUserStaffResp: + properties: avatar: description: avatar type: string - bio: - description: bio markdown - type: string - bio_html: - description: bio html - type: string - created_at: - description: create time - type: integer display_name: description: display name type: string - e_mail: - description: email - type: string - follow_count: - description: follow count - type: integer - id: - description: user id - type: string - ip_info: - description: ip info - type: string - is_admin: - description: is admin - type: boolean - last_login_date: - description: last login date - type: integer - location: - description: location - type: string - mail_status: - description: mail status(1 pass 2 to be verified) - type: integer - mobile: - description: mobile - type: string - notice_status: - description: notice status(1 on 2off) - type: integer - question_count: - description: question count - type: integer - rank: - description: rank - type: integer - status: - description: user status - type: string username: description: username type: string - website: - description: website - type: string type: object schema.GetVoteWithPageResp: properties: @@ -699,10 +1464,27 @@ definitions: title: description: title type: string + url_title: + description: url title + type: string vote_type: description: vote type type: string type: object + schema.LoadingAction: + properties: + state: + type: string + text: + type: string + type: object + schema.NotificationChannelConfig: + properties: + enable: + type: boolean + key: + $ref: '#/definitions/constant.NotificationChannelKey' + type: object schema.NotificationClearIDRequest: properties: id: @@ -711,9 +1493,55 @@ definitions: schema.NotificationClearRequest: properties: type: - description: inbox achievement + enum: + - inbox + - achievement + type: string + required: + - type + type: object + schema.OnCompleteAction: + properties: + refresh_form_config: + type: boolean + toast_return_message: + type: boolean + type: object + schema.Operation: + properties: + description: + type: string + level: + $ref: '#/definitions/schema.OperationLevel' + msg: + type: string + time: + type: integer + type: type: string type: object + schema.OperationLevel: + enum: + - info + - danger + - warning + - secondary + type: string + x-enum-varnames: + - OperationLevelInfo + - OperationLevelDanger + - OperationLevelWarning + - OperationLevelSecondary + schema.OperationQuestionReq: + properties: + id: + type: string + operation: + description: operation [pin unpin hide show] + type: string + required: + - id + type: object schema.PermissionMemberAction: properties: action: @@ -723,18 +1551,81 @@ definitions: type: type: string type: object - schema.QuestionAdd: + schema.PostRenderReq: properties: + content: + type: string + type: object + schema.PrivilegeLevel: + enum: + - 1 + - 2 + - 3 + - 99 + type: integer + x-enum-varnames: + - PrivilegeLevel1 + - PrivilegeLevel2 + - PrivilegeLevel3 + - PrivilegeLevelCustom + schema.PrivilegeOption: + properties: + level: + $ref: '#/definitions/schema.PrivilegeLevel' + level_desc: + type: string + privileges: + items: + $ref: '#/definitions/constant.Privilege' + type: array + type: object + schema.QuestionAdd: + properties: + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string content: description: content maxLength: 65535 minLength: 6 type: string - html: - description: html + tags: + description: tags + items: + $ref: '#/definitions/schema.TagItem' + type: array + title: + description: question title + maxLength: 150 + minLength: 6 + type: string + required: + - content + - tags + - title + type: object + schema.QuestionAddByAnswer: + properties: + answer_content: maxLength: 65535 minLength: 6 type: string + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string + content: + description: content + maxLength: 65535 + minLength: 6 + type: string + mention_username_list: + items: + type: string + type: array tags: description: tags items: @@ -746,33 +1637,192 @@ definitions: minLength: 6 type: string required: + - answer_content - content - - html - tags - title type: object - schema.QuestionSearch: + schema.QuestionInfoResp: + properties: + accepted_answer_id: + type: string + answer_count: + type: integer + answered: + type: boolean + collected: + type: boolean + collection_count: + type: integer + content: + type: string + create_time: + type: integer + description: + type: string + edit_time: + type: integer + extends_actions: + items: + $ref: '#/definitions/schema.PermissionMemberAction' + type: array + first_answer_id: + type: string + follow_count: + type: integer + html: + type: string + id: + type: string + is_followed: + type: boolean + last_answer_id: + type: string + last_answered_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + member_actions: + description: MemberActions + items: + $ref: '#/definitions/schema.PermissionMemberAction' + type: array + operation: + $ref: '#/definitions/schema.Operation' + pin: + type: integer + show: + type: integer + status: + type: integer + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + unique_view_count: + type: integer + update_time: + type: integer + update_user_info: + $ref: '#/definitions/schema.UserBasicInfo' + url_title: + type: string + user_info: + $ref: '#/definitions/schema.UserBasicInfo' + view_count: + type: integer + vote_count: + type: integer + vote_status: + type: string + type: object + schema.QuestionPageReq: properties: + in_days: + minimum: 1 + type: integer order: - description: Search order by + enum: + - newest + - active + - hot + - score + - unanswered + - recommend + - frequent type: string page: - description: Query number of pages + minimum: 1 type: integer page_size: - description: Search page size + minimum: 1 + type: integer + tag: + maxLength: 100 + type: string + username: + maxLength: 100 + type: string + type: object + schema.QuestionPageResp: + properties: + accepted_answer_id: + description: answer information + type: string + answer_count: + type: integer + collection_count: + type: integer + created_at: + type: integer + description: + type: string + follow_count: + type: integer + id: + type: string + last_answer_id: + type: string + operated_at: + description: operator information + type: integer + operation_type: + type: string + operator: + $ref: '#/definitions/schema.QuestionPageRespOperator' + pin: + description: '1: unpin, 2: pin' + type: integer + show: + description: '0: show, 1: hide' + type: integer + status: type: integer tags: - description: Search tag items: - type: string + $ref: '#/definitions/schema.TagResp' type: array + title: + type: string + unique_view_count: + type: integer + url_title: + type: string + view_count: + description: question statistical information + type: integer + vote_count: + type: integer + type: object + schema.QuestionPageRespOperator: + properties: + avatar: + type: string + display_name: + type: string + id: + type: string + rank: + type: integer + status: + type: string username: - description: Search username type: string type: object + schema.QuestionRecoverReq: + properties: + question_id: + type: string + required: + - question_id + type: object schema.QuestionUpdate: properties: + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string content: description: content maxLength: 65535 @@ -781,14 +1831,13 @@ definitions: edit_summary: description: edit summary type: string - html: - description: html - maxLength: 65535 - minLength: 6 - type: string id: description: question id type: string + invite_user: + items: + type: string + type: array tags: description: tags items: @@ -801,21 +1850,87 @@ definitions: type: string required: - content - - html - id - tags - title type: object + schema.QuestionUpdateInviteUser: + properties: + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string + id: + type: string + invite_user: + items: + type: string + type: array + required: + - id + type: object + schema.ReactionRespItem: + properties: + count: + description: Count is the number of users who reacted + type: integer + emoji: + description: Emoji is the reaction emoji + type: string + is_active: + description: IsActive is if current user has reacted + type: boolean + tooltip: + description: Tooltip is the user's name who reacted + type: string + type: object + schema.ReasonItem: + properties: + content_type: + type: string + description: + type: string + name: + type: string + placeholder: + type: string + reason_key: + type: string + reason_type: + type: integer + type: object + schema.RecoverAnswerReq: + properties: + answer_id: + type: string + required: + - answer_id + type: object + schema.RecoverTagReq: + properties: + tag_id: + type: string + required: + - tag_id + type: object schema.RemoveAnswerReq: properties: + captcha_code: + type: string + captcha_id: + type: string id: - description: answer id type: string required: - id type: object schema.RemoveCommentReq: properties: + captcha_code: + type: string + captcha_id: + type: string comment_id: description: comment id type: string @@ -824,6 +1939,11 @@ definitions: type: object schema.RemoveQuestionReq: properties: + captcha_code: + type: string + captcha_id: + description: captcha_id + type: string id: description: question id type: string @@ -838,31 +1958,56 @@ definitions: required: - tag_id type: object - schema.ReportHandleReq: + schema.ReopenQuestionReq: properties: - flagged_content: - type: string - flagged_type: - type: integer - id: + question_id: type: string - required: - - flagged_type - - id type: object - schema.SearchListResp: + schema.ReviewReportReq: properties: - count: + close_msg: + type: string + close_type: type: integer - extra: - description: extra fields - list: - description: search response + content: + maxLength: 65535 + minLength: 6 + type: string + flag_id: + type: string + operation_type: + enum: + - edit_post + - close_post + - delete_post + - unlist_post + - ignore_report + type: string + tags: items: - $ref: '#/definitions/schema.SearchResp' + $ref: '#/definitions/schema.TagItem' type: array + title: + maxLength: 150 + minLength: 6 + type: string + required: + - flag_id + - operation_type type: object - schema.SearchObject: + schema.RevisionAuditReq: + properties: + id: + description: object id + type: string + operation: + description: approve or reject + type: string + required: + - id + - operation + type: object + schema.SearchObject: properties: accepted: type: boolean @@ -874,6 +2019,8 @@ definitions: type: string id: type: string + question_id: + type: string status: description: Status type: string @@ -884,23 +2031,128 @@ definitions: type: array title: type: string + url_title: + type: string user_info: - $ref: '#/definitions/schema.UserBasicInfo' + allOf: + - $ref: '#/definitions/schema.SearchObjectUser' description: user info vote_count: type: integer type: object + schema.SearchObjectUser: + properties: + display_name: + type: string + id: + type: string + rank: + type: integer + status: + type: string + username: + type: string + type: object schema.SearchResp: + properties: + count: + type: integer + list: + description: search response + items: + $ref: '#/definitions/schema.SearchResult' + type: array + type: object + schema.SearchResult: properties: object: - $ref: '#/definitions/schema.SearchObject' + allOf: + - $ref: '#/definitions/schema.SearchObject' description: this object object_type: description: object_type type: string type: object + schema.SendUserActivationReq: + properties: + user_id: + type: string + required: + - user_id + type: object + schema.SiteBrandingReq: + properties: + favicon: + maxLength: 512 + type: string + logo: + maxLength: 512 + type: string + mobile_logo: + maxLength: 512 + type: string + square_icon: + maxLength: 512 + type: string + type: object + schema.SiteBrandingResp: + properties: + favicon: + maxLength: 512 + type: string + logo: + maxLength: 512 + type: string + mobile_logo: + maxLength: 512 + type: string + square_icon: + maxLength: 512 + type: string + type: object + schema.SiteCustomCssHTMLReq: + properties: + custom_css: + maxLength: 65536 + type: string + custom_footer: + maxLength: 65536 + type: string + custom_head: + maxLength: 65536 + type: string + custom_header: + maxLength: 65536 + type: string + custom_sidebar: + maxLength: 65536 + type: string + type: object + schema.SiteCustomCssHTMLResp: + properties: + custom_css: + maxLength: 65536 + type: string + custom_footer: + maxLength: 65536 + type: string + custom_head: + maxLength: 65536 + type: string + custom_header: + maxLength: 65536 + type: string + custom_sidebar: + maxLength: 65536 + type: string + type: object schema.SiteGeneralReq: properties: + check_update: + type: boolean + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -910,13 +2162,21 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: - - description + - contact_email - name - - short_description + - site_url type: object schema.SiteGeneralResp: properties: + check_update: + type: boolean + contact_email: + maxLength: 512 + type: string description: maxLength: 2000 type: string @@ -926,382 +2186,1397 @@ definitions: short_description: maxLength: 255 type: string + site_url: + maxLength: 512 + type: string required: - - description + - contact_email - name - - short_description + - site_url + type: object + schema.SiteInfoResp: + properties: + branding: + $ref: '#/definitions/schema.SiteBrandingResp' + custom_css_html: + $ref: '#/definitions/schema.SiteCustomCssHTMLResp' + general: + $ref: '#/definitions/schema.SiteGeneralResp' + interface: + $ref: '#/definitions/schema.SiteInterfaceResp' + login: + $ref: '#/definitions/schema.SiteLoginResp' + revision: + type: string + site_legal: + $ref: '#/definitions/schema.SiteLegalSimpleResp' + site_seo: + $ref: '#/definitions/schema.SiteSeoResp' + site_users: + $ref: '#/definitions/schema.SiteUsersResp' + site_write: + $ref: '#/definitions/schema.SiteWriteResp' + theme: + $ref: '#/definitions/schema.SiteThemeResp' + version: + type: string type: object schema.SiteInterfaceReq: properties: + default_avatar: + enum: + - system + - gravatar + type: string + gravatar_base_url: + type: string language: maxLength: 128 type: string - logo: - maxLength: 256 - type: string - theme: + time_zone: maxLength: 128 type: string required: + - default_avatar - language - - theme + - time_zone type: object schema.SiteInterfaceResp: properties: + default_avatar: + enum: + - system + - gravatar + type: string + gravatar_base_url: + type: string language: maxLength: 128 type: string - logo: - maxLength: 256 - type: string - theme: + time_zone: maxLength: 128 type: string required: + - default_avatar - language - - theme + - time_zone type: object - schema.TagItem: + schema.SiteLegalReq: properties: - display_name: - description: display_name - maxLength: 35 + external_content_display: + enum: + - always_display + - ask_before_display type: string - original_text: - description: original text + privacy_policy_original_text: type: string - parsed_text: - description: parsed text + privacy_policy_parsed_text: type: string - slug_name: - description: slug_name - maxLength: 35 + terms_of_service_original_text: type: string + terms_of_service_parsed_text: + type: string + required: + - external_content_display type: object - schema.TagResp: + schema.SiteLegalResp: properties: - display_name: + external_content_display: + enum: + - always_display + - ask_before_display type: string - main_tag_slug_name: - description: if main tag slug name is not empty, this tag is synonymous with - the main tag + privacy_policy_original_text: type: string - slug_name: + privacy_policy_parsed_text: type: string - type: object - schema.UpdateCommentReq: - properties: - comment_id: - description: comment id + terms_of_service_original_text: type: string - original_text: - description: original comment content + terms_of_service_parsed_text: type: string - parsed_text: - description: parsed comment content + required: + - external_content_display + type: object + schema.SiteLegalSimpleResp: + properties: + external_content_display: + enum: + - always_display + - ask_before_display type: string required: - - comment_id + - external_content_display type: object - schema.UpdateFollowTagsReq: + schema.SiteLoginReq: properties: - slug_name_list: - description: tag slug name list + allow_email_domains: items: type: string type: array + allow_email_registrations: + type: boolean + allow_new_registrations: + type: boolean + allow_password_login: + type: boolean + login_required: + type: boolean type: object - schema.UpdateInfoRequest: + schema.SiteLoginResp: properties: - avatar: - description: avatar - maxLength: 500 - type: string - bio: - description: bio - maxLength: 4096 - type: string - bio_html: - description: bio - maxLength: 4096 - type: string - display_name: - description: display_name - maxLength: 30 - type: string - location: - description: location - maxLength: 100 - type: string - username: - description: username - maxLength: 30 - type: string - website: - description: website - maxLength: 500 + allow_email_domains: + items: + type: string + type: array + allow_email_registrations: + type: boolean + allow_new_registrations: + type: boolean + allow_password_login: + type: boolean + login_required: + type: boolean + type: object + schema.SiteSeoReq: + properties: + permalink: + maximum: 4 + minimum: 0 + type: integer + robots: type: string required: - - display_name + - permalink + - robots type: object - schema.UpdateSMTPConfigReq: + schema.SiteSeoResp: properties: - encryption: - description: '"" SSL' - enum: - - SSL - type: string - from_email: - maxLength: 256 - type: string - from_name: - maxLength: 256 - type: string - smtp_authentication: - type: boolean - smtp_host: - maxLength: 256 - type: string - smtp_password: - maxLength: 256 - type: string - smtp_port: - maximum: 65535 - minimum: 1 + permalink: + maximum: 4 + minimum: 0 type: integer - smtp_username: - maxLength: 256 - type: string - test_email_recipient: + robots: type: string + required: + - permalink + - robots type: object - schema.UpdateTagReq: + schema.SiteThemeReq: properties: - display_name: - description: display_name - maxLength: 35 - type: string - edit_summary: - description: edit summary - type: string - original_text: - description: original text - type: string - parsed_text: - description: parsed text - type: string - slug_name: - description: slug_name - maxLength: 35 + color_scheme: + maxLength: 100 type: string - tag_id: - description: tag_id + theme: + maxLength: 255 type: string + theme_config: + additionalProperties: true + type: object required: - - tag_id + - theme type: object - schema.UpdateTagSynonymReq: + schema.SiteThemeResp: properties: - synonym_tag_list: - description: synonym tag list + color_scheme: + type: string + theme: + type: string + theme_config: + additionalProperties: true + type: object + theme_options: items: - $ref: '#/definitions/schema.TagItem' + $ref: '#/definitions/schema.ThemeOption' type: array - tag_id: - description: tag_id - type: string - required: - - synonym_tag_list - - tag_id type: object - schema.UpdateUserStatusReq: + schema.SiteUsersReq: properties: - status: - description: user status + allow_update_avatar: + type: boolean + allow_update_bio: + type: boolean + allow_update_display_name: + type: boolean + allow_update_location: + type: boolean + allow_update_username: + type: boolean + allow_update_website: + type: boolean + default_avatar: enum: - - normal - - suspended - - deleted - - inactive + - system + - gravatar type: string - user_id: - description: user id + gravatar_base_url: type: string required: - - status - - user_id + - default_avatar type: object - schema.UserBasicInfo: + schema.SiteUsersResp: properties: - avatar: - description: avatar - type: string - display_name: - description: display_name - type: string - ip_info: - description: ip info + allow_update_avatar: + type: boolean + allow_update_bio: + type: boolean + allow_update_display_name: + type: boolean + allow_update_location: + type: boolean + allow_update_username: + type: boolean + allow_update_website: + type: boolean + default_avatar: + enum: + - system + - gravatar type: string - location: - description: location + gravatar_base_url: type: string - rank: - description: rank + required: + - default_avatar + type: object + schema.SiteWriteReq: + properties: + authorized_attachment_extensions: + items: + type: string + type: array + authorized_image_extensions: + items: + type: string + type: array + max_attachment_size: type: integer - status: - description: status - type: string - username: - description: name + max_image_megapixel: + type: integer + max_image_size: + type: integer + recommend_tags: + items: + $ref: '#/definitions/schema.SiteWriteTag' + type: array + required_tag: + type: boolean + reserved_tags: + items: + $ref: '#/definitions/schema.SiteWriteTag' + type: array + restrict_answer: + type: boolean + type: object + schema.SiteWriteResp: + properties: + authorized_attachment_extensions: + items: + type: string + type: array + authorized_image_extensions: + items: + type: string + type: array + max_attachment_size: + type: integer + max_image_megapixel: + type: integer + max_image_size: + type: integer + recommend_tags: + items: + $ref: '#/definitions/schema.SiteWriteTag' + type: array + required_tag: + type: boolean + reserved_tags: + items: + $ref: '#/definitions/schema.SiteWriteTag' + type: array + restrict_answer: + type: boolean + type: object + schema.SiteWriteTag: + properties: + display_name: type: string - website: - description: website + slug_name: type: string + required: + - slug_name type: object - schema.UserChangeEmailSendCodeReq: + schema.TagItem: properties: - e_mail: - maxLength: 500 + display_name: + description: display_name + maxLength: 35 + type: string + original_text: + description: original text + type: string + slug_name: + description: slug_name + maxLength: 35 type: string - required: - - e_mail type: object - schema.UserChangeEmailVerifyReq: + schema.TagResp: properties: - code: - maxLength: 500 + display_name: + type: string + main_tag_slug_name: + description: if main tag slug name is not empty, this tag is synonymous with + the main tag + type: string + recommend: + type: boolean + reserved: + type: boolean + slug_name: type: string - required: - - code type: object - schema.UserEmailLogin: + schema.TagSynonym: properties: - captcha_code: - description: captcha_code + display_name: + description: display name type: string - captcha_id: - description: captcha_id + main_tag_slug_name: + description: if main tag slug name is not empty, this tag is synonymous with + the main tag type: string - e_mail: - description: e_mail + slug_name: + description: slug name type: string - pass: - description: password + tag_id: + description: tag id type: string type: object - schema.UserModifyPassWordRequest: + schema.ThemeOption: properties: - old_pass: - description: old password + label: type: string - pass: - description: password + value: type: string type: object - schema.UserNoticeSetRequest: + schema.UIOptionAction: properties: - notice_switch: - type: boolean + loading: + $ref: '#/definitions/schema.LoadingAction' + method: + type: string + on_complete: + $ref: '#/definitions/schema.OnCompleteAction' + url: + type: string type: object - schema.UserNoticeSetResp: + schema.UnreviewedRevisionInfoInfo: properties: - notice_switch: + answer_accepted: type: boolean + answer_count: + type: integer + answer_id: + type: string + comment_id: + type: string + content: + type: string + created_at: + type: integer + html: + type: string + object_creator_user_id: + type: string + object_id: + type: string + object_type: + type: string + question_id: + type: string + show_status: + type: integer + status: + type: integer + tags: + items: + $ref: '#/definitions/schema.TagResp' + type: array + title: + type: string + url_title: + type: string type: object - schema.UserRePassWordRequest: + schema.UpdateBadgeStatusReq: properties: - code: - description: code - maxLength: 100 - type: string - pass: - description: Password - maxLength: 32 + id: + description: badge id type: string + status: + allOf: + - $ref: '#/definitions/schema.BadgeStatus' + description: badge status required: - - code - - pass + - id + - status type: object - schema.UserRegisterReq: + schema.UpdateCommentReq: properties: - e_mail: - description: email - maxLength: 500 + captcha_code: type: string - name: - description: name - maxLength: 30 + captcha_id: + description: whether user can delete it type: string - pass: - description: password - maxLength: 32 - minLength: 8 + comment_id: + description: comment id + type: string + original_text: + description: original comment content + maxLength: 600 + minLength: 2 type: string required: - - e_mail - - name - - pass + - comment_id + - original_text type: object - schema.UserRetrievePassWordRequest: + schema.UpdateFollowTagsReq: properties: - captcha_code: - description: captcha_code + slug_name_list: + description: tag slug name list + items: + type: string + type: array + type: object + schema.UpdateInfoRequest: + properties: + avatar: + $ref: '#/definitions/schema.AvatarInfo' + bio: + maxLength: 4096 type: string - captcha_id: - description: captcha_id + display_name: + maxLength: 30 + minLength: 2 type: string - e_mail: - description: e_mail + location: + maxLength: 100 + type: string + username: + maxLength: 30 + minLength: 2 + type: string + website: maxLength: 500 type: string + type: object + schema.UpdatePluginConfigReq: + properties: + config_fields: + additionalProperties: {} + type: object + plugin_slug_name: + maxLength: 100 + type: string required: - - e_mail + - plugin_slug_name type: object - schema.VoteReq: + schema.UpdatePluginStatusReq: properties: - is_cancel: - description: is cancel + enabled: type: boolean + plugin_slug_name: + maxLength: 100 + type: string + required: + - plugin_slug_name + type: object + schema.UpdatePrivilegesConfigReq: + properties: + custom_privileges: + items: + $ref: '#/definitions/constant.Privilege' + type: array + level: + allOf: + - $ref: '#/definitions/schema.PrivilegeLevel' + minimum: 1 + required: + - level + type: object + schema.UpdateReactionReq: + properties: + emoji: + enum: + - heart + - smile + - frown + type: string object_id: - description: id + type: string + reaction: + enum: + - activate + - deactivate type: string required: + - emoji - object_id + - reaction type: object - schema.VoteResp: + schema.UpdateReviewReq: properties: - down_votes: - type: integer - up_votes: + review_id: type: integer - vote_status: + status: + enum: + - approve + - reject type: string - votes: - type: integer + required: + - review_id + - status type: object -info: - contact: {} -paths: - /answer/admin/api/answer/page: - get: - consumes: - - application/json - description: Status:[available,deleted] - parameters: - - description: page size - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: user status + schema.UpdateSMTPConfigReq: + properties: + encryption: + description: '"" SSL TLS' enum: - - available - - deleted + - SSL + - TLS + type: string + from_email: + maxLength: 256 + type: string + from_name: + maxLength: 256 + type: string + smtp_authentication: + type: boolean + smtp_host: + maxLength: 256 + type: string + smtp_password: + maxLength: 256 + type: string + smtp_port: + maximum: 65535 + minimum: 1 + type: integer + smtp_username: + maxLength: 256 + type: string + test_email_recipient: + type: string + type: object + schema.UpdateTagReq: + properties: + display_name: + description: display_name + maxLength: 35 + type: string + edit_summary: + description: edit summary + type: string + original_text: + description: original text + type: string + slug_name: + description: slug_name + maxLength: 35 + type: string + tag_id: + description: tag_id + type: string + required: + - tag_id + type: object + schema.UpdateTagSynonymReq: + properties: + synonym_tag_list: + description: synonym tag list + items: + $ref: '#/definitions/schema.TagItem' + type: array + tag_id: + description: tag_id + type: string + required: + - synonym_tag_list + - tag_id + type: object + schema.UpdateUserInterfaceRequest: + properties: + color_scheme: + description: Color scheme + maxLength: 100 + type: string + language: + description: language + maxLength: 100 + type: string + required: + - color_scheme + - language + type: object + schema.UpdateUserNotificationConfigReq: + properties: + all_new_question: + $ref: '#/definitions/schema.NotificationChannelConfig' + all_new_question_for_following_tags: + $ref: '#/definitions/schema.NotificationChannelConfig' + inbox: + $ref: '#/definitions/schema.NotificationChannelConfig' + type: object + schema.UpdateUserPasswordReq: + properties: + password: + maxLength: 32 + minLength: 8 + type: string + user_id: + type: string + required: + - password + - user_id + type: object + schema.UpdateUserPluginConfigReq: + properties: + config_fields: + additionalProperties: {} + type: object + plugin_slug_name: + maxLength: 100 + type: string + required: + - plugin_slug_name + type: object + schema.UpdateUserRoleReq: + properties: + role_id: + description: role id + type: integer + user_id: + description: user id + type: string + required: + - role_id + - user_id + type: object + schema.UpdateUserStatusReq: + properties: + remove_all_content: + type: boolean + status: + enum: + - normal + - suspended + - deleted + - inactive + type: string + suspend_duration: + enum: + - 24h + - 48h + - 72h + - 7d + - 14d + - 1m + - 2m + - 3m + - 6m + - 1y + - forever + type: string + user_id: + type: string + required: + - status + - user_id + type: object + schema.UserBasicInfo: + properties: + avatar: + type: string + display_name: + type: string + id: + type: string + language: + type: string + location: + type: string + rank: + type: integer + status: + type: string + suspended_until: + type: integer + username: + type: string + website: + type: string + type: object + schema.UserChangeEmailSendCodeReq: + properties: + captcha_code: + type: string + captcha_id: + type: string + e_mail: + maxLength: 500 + type: string + pass: + maxLength: 32 + minLength: 8 + type: string + required: + - e_mail + type: object + schema.UserChangeEmailVerifyReq: + properties: + code: + maxLength: 500 + type: string + required: + - code + type: object + schema.UserEmailLoginReq: + properties: + captcha_code: + type: string + captcha_id: + type: string + e_mail: + maxLength: 500 + type: string + pass: + maxLength: 32 + minLength: 8 + type: string + required: + - e_mail + - pass + type: object + schema.UserLoginResp: + properties: + access_token: + description: access token + type: string + answer_count: + description: answer count + type: integer + authority_group: + description: authority group + type: integer + avatar: + description: avatar + type: string + bio: + description: bio markdown + type: string + bio_html: + description: bio html + type: string + color_scheme: + description: Color scheme + type: string + created_at: + description: create time + type: integer + display_name: + description: display name + type: string + e_mail: + description: email + type: string + follow_count: + description: follow count + type: integer + have_password: + description: user have password + type: boolean + id: + description: user id + type: string + language: + description: language + type: string + last_login_date: + description: last login date + type: integer + location: + description: location + type: string + mail_status: + description: mail status(1 pass 2 to be verified) + type: integer + mobile: + description: mobile + type: string + notice_status: + description: notice status(1 on 2off) + type: integer + question_count: + description: question count + type: integer + rank: + description: rank + type: integer + role_id: + description: role id + type: integer + status: + description: user status + type: string + suspended_until: + description: suspended until timestamp + type: integer + username: + description: username + type: string + visit_token: + description: visit token + type: string + website: + description: website + type: string + type: object + schema.UserModifyPasswordReq: + properties: + captcha_code: + type: string + captcha_id: + type: string + old_pass: + maxLength: 32 + minLength: 8 + type: string + pass: + maxLength: 32 + minLength: 8 + type: string + required: + - pass + type: object + schema.UserRankingResp: + properties: + staffs: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + users_with_the_most_reputation: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + users_with_the_most_vote: + items: + $ref: '#/definitions/schema.UserRankingSimpleInfo' + type: array + type: object + schema.UserRankingSimpleInfo: + properties: + avatar: + description: avatar + type: string + display_name: + description: display name + type: string + rank: + description: rank + type: integer + username: + description: username + type: string + vote_count: + description: vote + type: integer + type: object + schema.UserRePassWordRequest: + properties: + code: + maxLength: 100 + type: string + pass: + maxLength: 32 + type: string + required: + - code + - pass + type: object + schema.UserRegisterReq: + properties: + captcha_code: + type: string + captcha_id: + type: string + e_mail: + maxLength: 500 + type: string + name: + maxLength: 30 + minLength: 2 + type: string + pass: + maxLength: 32 + minLength: 8 + type: string + required: + - e_mail + - name + - pass + type: object + schema.UserRetrievePassWordRequest: + properties: + captcha_code: + type: string + captcha_id: + type: string + e_mail: + maxLength: 500 + type: string + required: + - e_mail + type: object + schema.UserUnsubscribeNotificationReq: + properties: + code: + maxLength: 500 + type: string + required: + - code + type: object + schema.VoteReq: + properties: + captcha_code: + type: string + captcha_id: + type: string + is_cancel: + type: boolean + object_id: + type: string + required: + - object_id + type: object + schema.VoteResp: + properties: + down_votes: + type: integer + up_votes: + type: integer + vote_status: + type: string + votes: + type: integer + type: object + translator.LangOption: + properties: + label: + type: string + progress: + description: Translation completion percentage + type: integer + value: + type: string + type: object +info: + contact: {} + description: Apache Answer API + title: Apache Answer +paths: + /: + get: + consumes: + - application/json + description: if config file not exist try to redirect to install page + produces: + - application/json + responses: {} + summary: if config file not exist try to redirect to install page + tags: + - installation + /answer/admin/api/answer/page: + get: + consumes: + - application/json + description: Status:[available,deleted,pending] + parameters: + - description: page size + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: user status + enum: + - available + - deleted + - pending + in: query + name: status + type: string + - description: answer id or question title + in: query + name: query + type: string + - description: question id + in: query + name: question_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: AdminAnswerPage admin answer page + tags: + - admin + /answer/admin/api/answer/status: + put: + consumes: + - application/json + description: update answer status + parameters: + - description: AdminUpdateAnswerStatusReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AdminUpdateAnswerStatusReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update answer status + tags: + - admin + /answer/admin/api/badge/status: + put: + consumes: + - application/json + description: update badge status + parameters: + - description: UpdateBadgeStatusReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateBadgeStatusReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update badge status + tags: + - AdminBadge + /answer/admin/api/badges: + get: + consumes: + - application/json + description: list all badges by page + parameters: + - description: page + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: badge status + enum: + - "" + - active + - inactive + in: query + name: status + type: string + - description: search param + in: query + name: q + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetBadgeListPagedResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: list all badges by page + tags: + - AdminBadge + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin + /answer/admin/api/delete/permanently: + delete: + consumes: + - application/json + description: delete permanently + parameters: + - description: DeletePermanentlyReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.DeletePermanentlyReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: delete permanently + tags: + - admin + /answer/admin/api/language/options: + get: + description: Get language options + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: Get language options + tags: + - Lang + /answer/admin/api/plugin/config: + get: + description: get plugin config + parameters: + - description: plugin_slug_name + in: query + name: plugin_slug_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetPluginConfigResp' + type: object + security: + - ApiKeyAuth: [] + summary: get plugin config + tags: + - AdminPlugin + put: + consumes: + - application/json + description: update plugin config + parameters: + - description: UpdatePluginConfigReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdatePluginConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update plugin config + tags: + - AdminPlugin + /answer/admin/api/plugin/status: + put: + consumes: + - application/json + description: update plugin status + parameters: + - description: UpdatePluginStatusReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdatePluginStatusReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update plugin status + tags: + - AdminPlugin + /answer/admin/api/plugins: + get: + consumes: + - application/json + description: get plugin list + parameters: + - description: 'status: active/inactive' + in: query + name: status + type: string + - description: have config + in: query + name: have_config + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetPluginListResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get plugin list + tags: + - AdminPlugin + /answer/admin/api/question/page: + get: + consumes: + - application/json + description: Status:[available,closed,deleted,pending] + parameters: + - description: page size + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: user status + enum: + - available + - closed + - deleted + - pending + in: query + name: status + type: string + - description: question id or title + in: query + name: query + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: AdminQuestionPage admin question page + tags: + - admin + /answer/admin/api/question/status: + put: + consumes: + - application/json + description: update question status + parameters: + - description: AdminUpdateQuestionStatusReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AdminUpdateQuestionStatusReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update question status + tags: + - admin + /answer/admin/api/reasons: + get: + consumes: + - application/json + description: get reasons by object type and action + parameters: + - description: object_type + enum: + - question + - answer + - comment + - user in: query - name: status + name: object_type + required: true + type: string + - description: action + enum: + - status + - close + - flag + - review + in: query + name: action + required: true type: string produces: - application/json @@ -1312,21 +3587,347 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: CmsSearchList + summary: get reasons by object type and action + tags: + - reason + /answer/admin/api/roles: + get: + description: get role list + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetRoleResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get role list + tags: + - admin + /answer/admin/api/setting/privileges: + get: + description: GetPrivilegesConfig get privileges config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetPrivilegesConfigResp' + type: object + security: + - ApiKeyAuth: [] + summary: GetPrivilegesConfig get privileges config + tags: + - admin + put: + description: update privileges config + parameters: + - description: config + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdatePrivilegesConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update privileges config + tags: + - admin + /answer/admin/api/setting/smtp: + get: + description: GetSMTPConfig get smtp config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetSMTPConfigResp' + type: object + security: + - ApiKeyAuth: [] + summary: GetSMTPConfig get smtp config + tags: + - admin + put: + description: update smtp config + parameters: + - description: smtp config + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateSMTPConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update smtp config + tags: + - admin + /answer/admin/api/siteinfo/branding: + get: + description: get site interface + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteBrandingResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site interface + tags: + - admin + put: + description: update site info branding + parameters: + - description: branding info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteBrandingReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site info branding + tags: + - admin + /answer/admin/api/siteinfo/custom-css-html: + get: + description: get site info custom html css config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteCustomCssHTMLResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info custom html css config + tags: + - admin + put: + description: update site custom css html config + parameters: + - description: login info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteCustomCssHTMLReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site custom css html config + tags: + - admin + /answer/admin/api/siteinfo/general: + get: + description: get site general information + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteGeneralResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site general information + tags: + - admin + put: + description: update site general information + parameters: + - description: general + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteGeneralReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site general information + tags: + - admin + /answer/admin/api/siteinfo/interface: + get: + description: get site interface + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteInterfaceResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site interface + tags: + - admin + put: + description: update site info interface + parameters: + - description: general + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteInterfaceReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site info interface + tags: + - admin + /answer/admin/api/siteinfo/legal: + get: + description: Set the legal information for the site + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteLegalResp' + type: object + security: + - ApiKeyAuth: [] + summary: Set the legal information for the site tags: - admin - /answer/admin/api/answer/status: put: - consumes: + description: update site legal info + parameters: + - description: write info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteLegalReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site legal info + tags: + - admin + /answer/admin/api/siteinfo/login: + get: + description: get site info login config + produces: - application/json - description: Status:[available,deleted] + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteLoginResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info login config + tags: + - admin + put: + description: update site login parameters: - - description: AdminSetAnswerStatusRequest + - description: login info in: body name: data required: true schema: - $ref: '#/definitions/entity.AdminSetAnswerStatusRequest' + $ref: '#/definitions/schema.SiteLoginReq' produces: - application/json responses: @@ -1336,12 +3937,38 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSetAnswerStatus + summary: update site login tags: - admin - /answer/admin/api/language/options: + /answer/admin/api/siteinfo/seo: get: - description: Get language options + description: get site seo information + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteSeoResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site seo information + tags: + - admin + put: + description: update site seo information + parameters: + - description: seo + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteSeoReq' produces: - application/json responses: @@ -1351,33 +3978,211 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get language options + summary: update site seo information tags: - - Lang - /answer/admin/api/question/page: + - admin + /answer/admin/api/siteinfo/theme: + get: + description: get site info theme config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteThemeResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site info theme config + tags: + - admin + put: + description: update site custom css html config + parameters: + - description: login info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteThemeReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site custom css html config + tags: + - admin + /answer/admin/api/siteinfo/users: + get: + description: get site user config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteUsersResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site user config + tags: + - admin + put: + description: update site info config about users + parameters: + - description: users info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteUsersReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site info config about users + tags: + - admin + /answer/admin/api/siteinfo/write: + get: + description: get site interface + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SiteWriteResp' + type: object + security: + - ApiKeyAuth: [] + summary: get site interface + tags: + - admin + put: + description: update site write info + parameters: + - description: write info + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SiteWriteReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update site write info + tags: + - admin + /answer/admin/api/theme/options: get: + description: Get theme options + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: Get theme options + tags: + - admin + /answer/admin/api/user: + post: consumes: - application/json - description: Status:[available,closed,deleted] + description: add user parameters: - - description: page size - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: user status - enum: - - available - - closed - - deleted + - description: user + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AddUserReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add user + tags: + - admin + /answer/admin/api/user/activation: + get: + description: get user activation + parameters: + - description: user id in: query - name: status + name: user_id + required: true type: string produces: - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetUserActivationResp' + type: object + security: + - ApiKeyAuth: [] + summary: get user activation + tags: + - admin + /answer/admin/api/user/password: + put: + consumes: + - application/json + description: update user password + parameters: + - description: user + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserPasswordReq' + produces: + - application/json responses: "200": description: OK @@ -1385,21 +4190,21 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: CmsSearchList + summary: update user password tags: - admin - /answer/admin/api/question/status: + /answer/admin/api/user/profile: put: consumes: - application/json - description: Status:[available,closed,deleted] + description: edit user profile parameters: - - description: AdminSetQuestionStatusRequest + - description: user in: body name: data required: true schema: - $ref: '#/definitions/schema.AdminSetQuestionStatusRequest' + $ref: '#/definitions/schema.EditUserProfileReq' produces: - application/json responses: @@ -1409,35 +4214,21 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: AdminSetQuestionStatus + summary: edit user profile tags: - admin - /answer/admin/api/reasons: - get: + /answer/admin/api/user/role: + put: consumes: - application/json - description: get reasons by object type and action + description: update user role parameters: - - description: object_type - enum: - - question - - answer - - comment - - user - in: query - name: object_type - required: true - type: string - - description: action - enum: - - status - - close - - flag - - review - in: query - name: action + - description: user + in: body + name: data required: true - type: string + schema: + $ref: '#/definitions/schema.UpdateUserRoleReq' produces: - application/json responses: @@ -1447,21 +4238,21 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: get reasons by object type and action + summary: update user role tags: - - reason - /answer/admin/api/report/: + - admin + /answer/admin/api/user/status: put: consumes: - application/json - description: handle flag + description: update user parameters: - - description: flag + - description: user in: body name: data required: true schema: - $ref: '#/definitions/schema.ReportHandleReq' + $ref: '#/definitions/schema.UpdateUserStatusReq' produces: - application/json responses: @@ -1471,42 +4262,21 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - - ApiKeyAuth: [] - summary: handle flag + summary: update user tags: - admin - /answer/admin/api/reports/page: - get: + /answer/admin/api/users: + post: consumes: - application/json - description: list report records + description: add users parameters: - - description: status - enum: - - pending - - completed - in: query - name: status - required: true - type: string - - description: object_type - enum: - - all - - question - - answer - - comment - in: query - name: object_type + - description: user + in: body + name: data required: true - type: string - - description: page size - in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer + schema: + $ref: '#/definitions/schema.AddUsersReq' produces: - application/json responses: @@ -1516,13 +4286,59 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] + summary: add users + tags: + - admin + /answer/admin/api/users/activation: + post: + description: send user activation + parameters: + - description: SendUserActivationReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.SendUserActivationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: - ApiKeyAuth: [] - summary: list report page + summary: send user activation tags: - admin - /answer/admin/api/setting/smtp: + /answer/admin/api/users/page: get: - description: GetSMTPConfig get smtp config + description: get user page + parameters: + - description: page size + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: 'search query: email, username or id:[id]' + in: query + name: query + type: string + - description: staff user + in: query + name: staff + type: boolean + - description: user status + enum: + - suspended + - deleted + - inactive + in: query + name: status + type: string produces: - application/json responses: @@ -1533,37 +4349,68 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetSMTPConfigResp' + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + records: + items: + $ref: '#/definitions/schema.GetUserPageResp' + type: array + type: object type: object security: - ApiKeyAuth: [] - summary: GetSMTPConfig get smtp config + summary: get user page tags: - admin - put: - description: update smtp config + /answer/api/v1/activity/timeline: + get: + description: get object timeline parameters: - - description: smtp config - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.UpdateSMTPConfigReq' + - description: object id + in: query + name: object_id + type: string + - description: tag slug name + in: query + name: tag_slug_name + type: string + - description: object type + enum: + - question + - answer + - tag + in: query + name: object_type + type: string + - description: is show vote + in: query + name: show_vote + type: boolean produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: update smtp config + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetObjectTimelineResp' + type: object + summary: get object timeline tags: - - admin - /answer/admin/api/siteinfo/general: + - Comment + /answer/api/v1/activity/timeline/detail: get: - description: Get siteinfo general + description: get object timeline detail + parameters: + - description: revision id + in: query + name: revision_id + required: true + type: string produces: - application/json responses: @@ -1574,22 +4421,23 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.SiteGeneralResp' + $ref: '#/definitions/schema.GetObjectTimelineResp' type: object - security: - - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get object timeline detail tags: - - admin - put: - description: Get siteinfo interface + - Comment + /answer/api/v1/answer: + delete: + consumes: + - application/json + description: delete answer parameters: - - description: general + - description: answer in: body name: data required: true schema: - $ref: '#/definitions/schema.SiteGeneralReq' + $ref: '#/definitions/schema.RemoveAnswerReq' produces: - application/json responses: @@ -1599,45 +4447,43 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: delete answer tags: - - admin - /answer/admin/api/siteinfo/interface: - get: - description: Get siteinfo interface + - Answer + post: + consumes: + - application/json + description: add answer parameters: - - description: general + - description: add answer request in: body name: data required: true schema: - $ref: '#/definitions/schema.AddCommentReq' + $ref: '#/definitions/schema.AnswerAddReq' produces: - application/json responses: "200": description: OK schema: - allOf: - - $ref: '#/definitions/handler.RespBody' - - properties: - data: - $ref: '#/definitions/schema.SiteInterfaceResp' - type: object + $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: Add Answer tags: - - admin + - Answer put: - description: Get siteinfo interface + consumes: + - application/json + description: Update Answer parameters: - - description: general + - description: AnswerUpdateReq in: body name: data required: true schema: - $ref: '#/definitions/schema.SiteInterfaceReq' + $ref: '#/definitions/schema.AnswerUpdateReq' produces: - application/json responses: @@ -1647,12 +4493,21 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: Update Answer tags: - - admin - /answer/admin/api/theme/options: - get: - description: Get theme options + - Answer + /answer/api/v1/answer/acceptance: + post: + consumes: + - application/json + description: Accept Answer + parameters: + - description: AcceptAnswerReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AcceptAnswerReq' produces: - application/json responses: @@ -1662,61 +4517,60 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get theme options + summary: Accept Answer tags: - - admin - /answer/admin/api/user/status: - put: + - Answer + /answer/api/v1/answer/info: + get: consumes: - application/json - description: update user + description: Get Answer Detail parameters: - - description: user - in: body - name: data + - description: id + in: query + name: id required: true - schema: - $ref: '#/definitions/schema.UpdateUserStatusReq' + type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: update user + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetAnswerInfoResp' + type: object + summary: Get Answer Detail tags: - - admin - /answer/admin/api/users/page: + - Answer + /answer/api/v1/answer/page: get: - description: get user page + consumes: + - application/json + description: AnswerList
order (default or updated) parameters: - - description: page size + - description: question_id in: query - name: page - type: integer - - description: page size - in: query - name: page_size - type: integer - - description: username + name: question_id + required: true + type: string + - description: order in: query - name: username + name: order + required: true type: string - - description: email + - description: page in: query - name: e_mail + name: page + required: true type: string - - description: user status - enum: - - normal - - suspended - - deleted - - inactive + - description: page_size in: query - name: status + name: page_size + required: true type: string produces: - application/json @@ -1724,36 +4578,22 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/handler.RespBody' - - properties: - data: - allOf: - - $ref: '#/definitions/pager.PageModel' - - properties: - records: - items: - $ref: '#/definitions/schema.GetUserPageResp' - type: array - type: object - type: object - security: - - ApiKeyAuth: [] - summary: get user page + type: string + summary: AnswerList tags: - - admin - /answer/api/v1/answer: - delete: + - Answer + /answer/api/v1/answer/recover: + post: consumes: - application/json - description: delete answer + description: recover the deleted answer parameters: - description: answer in: body name: data required: true schema: - $ref: '#/definitions/schema.RemoveAnswerReq' + $ref: '#/definitions/schema.RecoverAnswerReq' produces: - application/json responses: @@ -1763,89 +4603,111 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: delete answer + summary: recover answer tags: - - api-answer - post: + - Answer + /answer/api/v1/badge: + get: consumes: - application/json - description: Insert Answer + description: get badge info parameters: - - description: AnswerAddReq - in: body - name: data + - default: string + description: id + in: query + name: id required: true - schema: - $ref: '#/definitions/schema.AnswerAddReq' + type: string produces: - application/json responses: "200": description: OK schema: - type: string - security: - - ApiKeyAuth: [] - summary: Insert Answer + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetBadgeInfoResp' + type: object + summary: get badge info tags: - - api-answer - put: + - api-badge + /answer/api/v1/badge/awards/page: + get: consumes: - application/json - description: Update Answer + description: get badge award list parameters: - - description: AnswerUpdateReq - in: body - name: data + - description: page + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: badge id + in: query + name: badge_id required: true - schema: - $ref: '#/definitions/schema.AnswerUpdateReq' + type: string + - description: only list the award by username + in: query + name: username + type: string produces: - application/json responses: "200": description: OK schema: - type: string - security: - - ApiKeyAuth: [] - summary: Update Answer + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetBadgeInfoResp' + type: object + summary: get badge award list tags: - - api-answer - /answer/api/v1/answer/acceptance: - post: + - api-badge + /answer/api/v1/badge/user/awards: + get: consumes: - application/json - description: Adopted + description: get user badge award list parameters: - - description: AnswerAdoptedReq - in: body - name: data + - description: user name + in: query + name: username required: true - schema: - $ref: '#/definitions/schema.AnswerAdoptedReq' + type: string produces: - application/json responses: "200": description: OK schema: - type: string - security: - - ApiKeyAuth: [] - summary: Adopted + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetUserBadgeAwardListResp' + type: array + type: object + summary: get user badge award list tags: - - api-answer - /answer/api/v1/answer/info: + - api-badge + /answer/api/v1/badge/user/awards/recent: get: consumes: - application/json - description: Get Answer + description: get user badge award list parameters: - - default: "1" - description: Answer TagID + - description: user name in: query - name: id + name: username required: true type: string produces: @@ -1854,34 +4716,39 @@ paths: "200": description: OK schema: - type: string - summary: Get Answer + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetUserBadgeAwardListResp' + type: array + type: object + summary: get user badge award list tags: - - api-answer - /answer/api/v1/answer/list: + - api-badge + /answer/api/v1/badges: get: consumes: - application/json - description: AnswerList
order (default or updated) - parameters: - - description: AnswerList - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AnswerList' + description: list all badges group by group produces: - application/json responses: "200": description: OK schema: - type: string - security: - - ApiKeyAuth: [] - summary: AnswerList + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetBadgeListResp' + type: array + type: object + summary: list all badges group by group tags: - - api-answer + - api-badge /answer/api/v1/collection/switch: post: consumes: @@ -2061,6 +4928,159 @@ paths: summary: get comment page tags: - Comment + /answer/api/v1/connector/binding/email: + post: + consumes: + - application/json + description: external login binding user send email + parameters: + - description: external login binding user send email + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp' + type: object + summary: external login binding user send email + tags: + - PluginConnector + /answer/api/v1/connector/info: + get: + description: get all enabled connectors + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.ConnectorInfoResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get all enabled connectors + tags: + - PluginConnector + /answer/api/v1/connector/user/info: + get: + description: get all connectors info about user + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.ConnectorUserInfoResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get all connectors info about user + tags: + - PluginConnector + /answer/api/v1/connector/user/unbinding: + delete: + consumes: + - application/json + description: unbind external user login + parameters: + - description: ExternalLoginUnbindingReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.ExternalLoginUnbindingReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: unbind external user login + tags: + - PluginConnector + /answer/api/v1/embed/config: + get: + consumes: + - application/json + description: get embed plugin config + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/plugin.EmbedConfig' + type: array + type: object + summary: get embed plugin config + tags: + - Plugin + /answer/api/v1/file: + post: + consumes: + - multipart/form-data + description: upload file + parameters: + - description: identify the source of the file upload + enum: + - post + - post_attachment + - avatar + - branding + in: formData + name: source + required: true + type: string + - description: file + in: formData + name: file + required: true + type: file + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + type: string + type: object + security: + - ApiKeyAuth: [] + summary: upload file + tags: + - Upload /answer/api/v1/follow: post: consumes: @@ -2143,11 +5163,58 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: Get language options tags: - Lang + /answer/api/v1/meta/reaction: + get: + consumes: + - application/json + description: get reaction for an object + parameters: + - description: object_id + in: query + name: object_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.ReactionRespItem' + type: object + summary: get reaction + tags: + - Meta + put: + consumes: + - application/json + description: update reaction. if not exist, add one + parameters: + - description: reaction + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateReactionReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add or update reaction + tags: + - Meta /answer/api/v1/notification/page: get: consumes: @@ -2170,6 +5237,16 @@ paths: name: type required: true type: string + - description: inbox_type + enum: + - all + - posts + - invites + - votes + in: query + name: inbox_type + required: true + type: string produces: - application/json responses: @@ -2270,11 +5347,83 @@ paths: summary: DelRedDot tags: - Notification + /answer/api/v1/permission: + get: + description: check user permission + parameters: + - description: access-token + in: header + name: Authorization + required: true + type: string + - description: permission key + enum: + - question.add + - question.edit + - question.edit_without_review + - question.delete + - question.close + - question.reopen + - question.vote_up + - question.vote_down + - question.pin + - question.unpin + - question.hide + - question.show + - answer.add + - answer.edit + - answer.edit_without_review + - answer.delete + - answer.accept + - answer.vote_up + - answer.vote_down + - answer.invite_someone_to_answer + - comment.add + - comment.edit + - comment.delete + - comment.vote_up + - comment.vote_down + - report.add + - tag.add + - tag.edit + - tag.edit_slug_name + - tag.edit_without_review + - tag.delete + - tag.synonym + - link.url_limit + - vote.detail + - answer.audit + - question.audit + - tag.audit + - tag.use_reserved_tag + in: query + name: action + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + additionalProperties: + type: boolean + type: object + type: object + security: + - ApiKeyAuth: [] + summary: check user permission + tags: + - Permission /answer/api/v1/personal/answer/page: get: consumes: - application/json - description: UserAnswerList + description: list personal answers parameters: - default: string description: username @@ -2297,9 +5446,9 @@ paths: required: true type: string - default: "20" - description: pagesize + description: page_size in: query - name: pagesize + name: page_size required: true type: string produces: @@ -2311,14 +5460,14 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: UserAnswerList + summary: list personal answers tags: - - api-answer + - Personal /answer/api/v1/personal/collection/page: get: consumes: - application/json - description: UserCollectionList + description: list personal collections parameters: - default: "0" description: page @@ -2327,9 +5476,9 @@ paths: required: true type: string - default: "20" - description: pagesize + description: page_size in: query - name: pagesize + name: page_size required: true type: string produces: @@ -2341,7 +5490,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: UserCollectionList + summary: list personal collections tags: - Collection /answer/api/v1/personal/comment/page: @@ -2401,11 +5550,9 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: UserTop tags: - - api-question + - Question /answer/api/v1/personal/rank/page: get: description: user personal rank list @@ -2437,7 +5584,7 @@ paths: - properties: list: items: - $ref: '#/definitions/schema.GetRankPersonalWithPageResp' + $ref: '#/definitions/schema.GetRankPersonalPageResp' type: array type: object type: object @@ -2476,7 +5623,7 @@ paths: get: consumes: - application/json - description: user's vote + description: get user personal votes parameters: - description: page size in: query @@ -2507,9 +5654,55 @@ paths: type: object security: - ApiKeyAuth: [] - summary: user's votes + summary: get user personal votes tags: - Activity + /answer/api/v1/plugin/status: + get: + consumes: + - application/json + description: get all plugins status + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetPluginListResp' + type: array + type: object + summary: get all plugins status + tags: + - Plugin + /answer/api/v1/post/render: + post: + consumes: + - application/json + description: render post content + parameters: + - description: PostRenderReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.PostRenderReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: render post content + tags: + - Upload /answer/api/v1/question: delete: consumes: @@ -2528,23 +5721,213 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: delete question + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: delete question + tags: + - Question + post: + consumes: + - application/json + description: add question + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionAdd' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add question + tags: + - Question + put: + consumes: + - application/json + description: update question + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionUpdate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update question + tags: + - Question + /answer/api/v1/question/answer: + post: + consumes: + - application/json + description: add question and answer + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionAddByAnswer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add question and answer + tags: + - Question + /answer/api/v1/question/info: + get: + consumes: + - application/json + description: get question details + parameters: + - default: "1" + description: Question TagID + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: get question details + tags: + - Question + /answer/api/v1/question/invite: + get: + consumes: + - application/json + description: get question invite user info + parameters: + - default: "1" + description: Question ID + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: get question invite user info + tags: + - Question + put: + consumes: + - application/json + description: update question invite user + parameters: + - description: question + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionUpdateInviteUser' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update question invite user + tags: + - Question + /answer/api/v1/question/link: + get: + description: get question link + parameters: + - in: query + minimum: 1 + name: in_days + type: integer + - enum: + - newest + - active + - hot + - score + - unanswered + - recommend + - frequent + in: query + name: order + type: string + - in: query + minimum: 1 + name: page + type: integer + - in: query + maximum: 100 + minimum: 1 + name: page_size + type: integer + - in: query + name: question_id + required: true + type: string + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.QuestionPageResp' + type: array + type: object + type: object + summary: get question link tags: - - api-question - post: + - Question + /answer/api/v1/question/operation: + put: consumes: - application/json - description: add question + description: Operation question \n operation [pin unpin hide show] parameters: - description: question in: body name: data required: true schema: - $ref: '#/definitions/schema.QuestionAdd' + $ref: '#/definitions/schema.OperationQuestionReq' produces: - application/json responses: @@ -2554,122 +5937,130 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: add question + summary: Operation question tags: - - api-question - put: + - Question + /answer/api/v1/question/page: + get: consumes: - application/json - description: update question + description: get questions by page parameters: - - description: question + - description: QuestionPageReq in: body name: data required: true schema: - $ref: '#/definitions/schema.QuestionUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: update question - tags: - - api-question - /answer/api/v1/question/closemsglist: - get: - consumes: - - application/json - description: close question msg list + $ref: '#/definitions/schema.QuestionPageReq' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: close question msg list + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.QuestionPageResp' + type: array + type: object + type: object + summary: get questions by page tags: - - api-question - /answer/api/v1/question/info: + - Question + /answer/api/v1/question/recommend/page: get: consumes: - application/json - description: GetQuestion Question + description: get recommend questions by page parameters: - - default: "1" - description: Question TagID - in: query - name: id + - description: QuestionPageReq + in: body + name: data required: true - type: string + schema: + $ref: '#/definitions/schema.QuestionPageReq' produces: - application/json responses: "200": description: OK schema: - type: string - security: - - ApiKeyAuth: [] - summary: GetQuestion Question + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.QuestionPageResp' + type: array + type: object + type: object + summary: get recommend questions by page tags: - - api-question - /answer/api/v1/question/page: - get: + - Question + /answer/api/v1/question/recover: + post: consumes: - application/json - description: SearchQuestionList
"order" Enums(newest, active,frequent,score,unanswered) + description: recover deleted question parameters: - - description: QuestionSearch + - description: question in: body name: data required: true schema: - $ref: '#/definitions/schema.QuestionSearch' + $ref: '#/definitions/schema.QuestionRecoverReq' produces: - application/json responses: "200": description: OK schema: - type: string - summary: SearchQuestionList + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: recover deleted question tags: - - api-question - /answer/api/v1/question/search: - post: + - Question + /answer/api/v1/question/reopen: + put: consumes: - application/json - description: SearchQuestionList + description: reopen question parameters: - - description: QuestionSearch + - description: question in: body name: data required: true schema: - $ref: '#/definitions/schema.QuestionSearch' + $ref: '#/definitions/schema.ReopenQuestionReq' produces: - application/json responses: "200": description: OK schema: - type: string - summary: SearchQuestionList + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: reopen question tags: - - api-question + - Question /answer/api/v1/question/similar: get: consumes: - application/json - description: add question title like + description: fuzzy query similar questions based on title parameters: - default: string description: title @@ -2686,9 +6077,9 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: add question title like + summary: fuzzy query similar questions based on title tags: - - api-question + - Question /answer/api/v1/question/similar/tag: get: consumes: @@ -2710,7 +6101,7 @@ paths: type: string summary: Search Similar Question tags: - - api-question + - Question /answer/api/v1/question/status: put: consumes: @@ -2734,7 +6125,7 @@ paths: - ApiKeyAuth: [] summary: Close question tags: - - api-question + - Question /answer/api/v1/question/tags: get: description: get tag list @@ -2754,7 +6145,7 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetTagResp' + $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object security: @@ -2800,18 +6191,228 @@ paths: summary: get reasons by object type and action tags: - reason + /answer/api/v1/render/config: + get: + consumes: + - application/json + description: GetRenderConfig + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/plugin.RenderConfig' + type: object + summary: GetRenderConfig + tags: + - PluginRender /answer/api/v1/report: post: consumes: - application/json - description: add report
source (question, answer, comment, user) + description: add report
source (question, answer, comment, user) + parameters: + - description: report + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AddReportReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add report + tags: + - Report + /answer/api/v1/report/review: + put: + consumes: + - application/json + description: review report + parameters: + - description: flag + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.ReviewReportReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: review report + tags: + - Report + /answer/api/v1/report/unreviewed/post: + get: + consumes: + - application/json + description: get unreviewed report post page + parameters: + - description: page + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetReportListPageResp' + type: array + type: object + type: object + security: + - ApiKeyAuth: [] + summary: get unreviewed report post page + tags: + - Report + /answer/api/v1/review/pending/post: + put: + consumes: + - application/json + description: update review + parameters: + - description: review + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateReviewReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update review + tags: + - Review + /answer/api/v1/review/pending/post/page: + get: + consumes: + - application/json + description: get unreviewed post page + parameters: + - description: page + in: query + name: page + type: integer + - description: object_id + in: query + name: object_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetUnreviewedPostPageResp' + type: array + type: object + type: object + security: + - ApiKeyAuth: [] + summary: get unreviewed post page + tags: + - Review + /answer/api/v1/reviewing/type: + get: + description: get reviewing type + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetReviewingTypeResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get reviewing type + tags: + - Revision + /answer/api/v1/revisions: + get: + description: get revision list + parameters: + - description: object id + in: query + name: object_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetRevisionResp' + type: array + type: object + summary: get revision list + tags: + - Revision + /answer/api/v1/revisions/audit: + put: + description: revision audit operation:approve or reject parameters: - - description: report + - description: audit in: body name: data required: true schema: - $ref: '#/definitions/schema.AddReportReq' + $ref: '#/definitions/schema.RevisionAuditReq' produces: - application/json responses: @@ -2821,22 +6422,19 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - - ApiKeyAuth: [] - summary: add report + summary: revision audit tags: - - Report - /answer/api/v1/report/type/list: + - Revision + /answer/api/v1/revisions/edit/check: get: - description: get report type list + consumes: + - application/json + description: check can update revision parameters: - - description: report source - enum: - - question - - answer - - comment - - user + - default: string + description: id in: query - name: source + name: id required: true type: string produces: @@ -2845,24 +6443,19 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/handler.RespBody' - - properties: - data: - items: - $ref: '#/definitions/schema.GetReportTypeResp' - type: array - type: object - summary: get report type list + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: check can update revision tags: - - Report - /answer/api/v1/revisions: + - Revision + /answer/api/v1/revisions/unreviewed: get: - description: get revision list + description: get unreviewed revision list parameters: - - description: object id + - description: page id in: query - name: object_id + name: page required: true type: string produces: @@ -2875,11 +6468,18 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - items: - $ref: '#/definitions/schema.GetRevisionResp' - type: array + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.GetUnreviewedRevisionResp' + type: array + type: object type: object - summary: get revision list + security: + - ApiKeyAuth: [] + summary: get unreviewed revision list tags: - Revision /answer/api/v1/search: @@ -2911,16 +6511,34 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.SearchListResp' + $ref: '#/definitions/schema.SearchResp' type: object security: - ApiKeyAuth: [] summary: search object tags: - Search + /answer/api/v1/search/desc: + get: + description: get search description + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.SearchResp' + type: object + summary: get search description + tags: + - Search /answer/api/v1/siteinfo: get: - description: Get siteinfo + description: get site info produces: - application/json responses: @@ -2931,9 +6549,36 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.SiteGeneralResp' + $ref: '#/definitions/schema.SiteInfoResp' + type: object + summary: get site info + tags: + - site + /answer/api/v1/siteinfo/legal: + get: + description: get site legal info + parameters: + - description: legal information type + enum: + - tos + - privacy + in: query + name: info_type + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetSiteLegalInfoResp' type: object - summary: Get siteinfo + summary: get site legal info tags: - site /answer/api/v1/tag: @@ -2955,6 +6600,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: delete tag tags: - Tag @@ -2988,6 +6635,29 @@ paths: summary: get tag one tags: - Tag + post: + consumes: + - application/json + description: add tag + parameters: + - description: tag + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AddTagReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: add tag + tags: + - Tag put: consumes: - application/json @@ -3006,9 +6676,59 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: update tag tags: - Tag + /answer/api/v1/tag/merge: + post: + consumes: + - application/json + description: merge tag + parameters: + - description: tag + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.AddTagReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: merge tag + tags: + - Tag + /answer/api/v1/tag/recover: + post: + consumes: + - application/json + description: recover delete tag + parameters: + - description: tag + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.RecoverTagReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: recover delete tag + tags: + - Tag /answer/api/v1/tag/synonym: put: consumes: @@ -3028,6 +6748,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: update tag tags: - Tag @@ -3042,6 +6764,32 @@ paths: type: integer produces: - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetTagSynonymsResp' + type: object + summary: get tag synonyms + tags: + - Tag + /answer/api/v1/tags: + get: + description: get tags list by slug name + parameters: + - collectionFormat: csv + description: string collection + in: query + items: + type: string + name: tags + type: array + produces: + - application/json responses: "200": description: OK @@ -3051,10 +6799,10 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetTagSynonymsResp' + $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object - summary: get tag synonyms + summary: get tags list tags: - Tag /answer/api/v1/tags/following: @@ -3153,32 +6901,6 @@ paths: summary: ActionRecord tags: - User - /answer/api/v1/user/avatar/upload: - post: - consumes: - - multipart/form-data - description: UserUpdateInfo - parameters: - - description: file - in: formData - name: file - required: true - type: file - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/handler.RespBody' - - properties: - data: - type: string - type: object - security: - - ApiKeyAuth: [] - summary: UserUpdateInfo - tags: - - User /answer/api/v1/user/email: put: consumes: @@ -3222,6 +6944,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: send email to the user email then change their email tags: - User @@ -3247,7 +6971,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserVerifyEmail tags: @@ -3274,17 +6998,74 @@ paths: "200": description: OK schema: - type: string + type: string + security: + - ApiKeyAuth: [] + summary: UserVerifyEmailSend + tags: + - User + /answer/api/v1/user/info: + get: + consumes: + - application/json + description: get user info, if user no login response http code is 200, but + user info is null + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetCurrentLoginUserInfoResp' + type: object + security: + - ApiKeyAuth: [] + summary: GetUserInfoByUserID + tags: + - User + put: + consumes: + - application/json + description: UserUpdateInfo update user info + parameters: + - description: access-token + in: header + name: Authorization + required: true + type: string + - description: UpdateInfoRequest + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateInfoRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: UserVerifyEmailSend + summary: UserUpdateInfo update user info tags: - User - /answer/api/v1/user/info: + /answer/api/v1/user/info/search: get: consumes: - application/json - description: GetUserInfoByUserID + description: SearchUserListByName + parameters: + - description: username + in: query + name: username + required: true + type: string produces: - application/json responses: @@ -3295,17 +7076,18 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.GetOtherUserInfoResp' type: object security: - ApiKeyAuth: [] - summary: GetUserInfoByUserID + summary: SearchUserListByName tags: - User + /answer/api/v1/user/interface: put: consumes: - application/json - description: UserUpdateInfo update user info + description: UserUpdateInterface update user interface config parameters: - description: access-token in: header @@ -3317,7 +7099,7 @@ paths: name: data required: true schema: - $ref: '#/definitions/schema.UpdateInfoRequest' + $ref: '#/definitions/schema.UpdateUserInterfaceRequest' produces: - application/json responses: @@ -3327,7 +7109,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: UserUpdateInfo update user info + summary: UserUpdateInterface update user interface config tags: - User /answer/api/v1/user/login/email: @@ -3341,7 +7123,7 @@ paths: name: data required: true schema: - $ref: '#/definitions/schema.UserEmailLogin' + $ref: '#/definitions/schema.UserEmailLoginReq' produces: - application/json responses: @@ -3352,7 +7134,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserEmailLogin tags: @@ -3369,21 +7151,16 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: user logout tags: - User - /answer/api/v1/user/notice/set: + /answer/api/v1/user/notification/config: post: consumes: - application/json - description: UserNoticeSet - parameters: - - description: UserNoticeSetRequest - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.UserNoticeSetRequest' + description: get user's notification config produces: - application/json responses: @@ -3394,11 +7171,56 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.UserNoticeSetResp' + $ref: '#/definitions/schema.GetUserNotificationConfigResp' type: object security: - ApiKeyAuth: [] - summary: UserNoticeSet + summary: get user's notification config + tags: + - User + put: + consumes: + - application/json + description: update user's notification config + parameters: + - description: UpdateUserNotificationConfigReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserNotificationConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update user's notification config + tags: + - User + /answer/api/v1/user/notification/unsubscribe: + put: + consumes: + - application/json + description: unsubscribe notification + parameters: + - description: UserUnsubscribeNotificationReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UserUnsubscribeNotificationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + summary: unsubscribe notification tags: - User /answer/api/v1/user/password: @@ -3407,12 +7229,12 @@ paths: - application/json description: UserModifyPassWord parameters: - - description: UserModifyPassWordRequest + - description: UserModifyPasswordReq in: body name: data required: true schema: - $ref: '#/definitions/schema.UserModifyPassWordRequest' + $ref: '#/definitions/schema.UserModifyPasswordReq' produces: - application/json responses: @@ -3469,17 +7291,62 @@ paths: summary: RetrievePassWord tags: - User - /answer/api/v1/user/post/file: - post: + /answer/api/v1/user/plugin/config: + get: + description: get user plugin config + parameters: + - description: plugin_slug_name + in: query + name: plugin_slug_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetPluginConfigResp' + type: object + security: + - ApiKeyAuth: [] + summary: get user plugin config + tags: + - UserPlugin + put: consumes: - - multipart/form-data - description: upload user post file + - application/json + description: update user plugin config parameters: - - description: file - in: formData - name: file + - description: UpdatePluginConfigReq + in: body + name: data required: true - type: file + schema: + $ref: '#/definitions/schema.UpdateUserPluginConfigReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update user plugin config + tags: + - UserPlugin + /answer/api/v1/user/plugin/configs: + get: + consumes: + - application/json + description: get plugin list that used for user. + produces: + - application/json responses: "200": description: OK @@ -3488,11 +7355,33 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - type: string + items: + $ref: '#/definitions/schema.GetUserPluginListResp' + type: array type: object security: - ApiKeyAuth: [] - summary: upload user post file + summary: get plugin list that used for user. + tags: + - UserPlugin + /answer/api/v1/user/ranking: + get: + consumes: + - application/json + description: get user ranking + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.UserRankingResp' + type: object + summary: get user ranking tags: - User /answer/api/v1/user/register/email: @@ -3517,16 +7406,27 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.UserLoginResp' type: object summary: UserRegisterByEmail tags: - User - /answer/api/v1/user/status: + /answer/api/v1/user/staff: get: consumes: - application/json - description: get user status info + description: get user staff + parameters: + - description: username + in: query + name: username + required: true + type: string + - description: page_size + in: query + name: page_size + required: true + type: string produces: - application/json responses: @@ -3537,11 +7437,9 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.GetUserStaffResp' type: object - security: - - ApiKeyAuth: [] - summary: get user status info + summary: get user staff tags: - User /answer/api/v1/vote/down: @@ -3602,11 +7500,154 @@ paths: summary: vote up tags: - Activity + /custom.css: + get: + description: get site custom CSS + produces: + - text/css + responses: + "200": + description: OK + schema: + type: string + summary: get site custom CSS + tags: + - site + /installation/base-info: + post: + consumes: + - application/json + description: init base info + parameters: + - description: InitBaseInfoReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/install.InitBaseInfoReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + summary: init base info + tags: + - installation + /installation/config-file/check: + post: + consumes: + - application/json + description: check config file if exist when installation + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/install.CheckConfigFileResp' + type: object + summary: check config file if exist when installation + tags: + - installation + /installation/db/check: + post: + consumes: + - application/json + description: check database if exist when installation + parameters: + - description: CheckDatabaseReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/install.CheckDatabaseReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/install.CheckConfigFileResp' + type: object + summary: check database if exist when installation + tags: + - installation + /installation/init: + post: + consumes: + - application/json + description: init environment + parameters: + - description: CheckDatabaseReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/install.CheckDatabaseReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + summary: init environment + tags: + - installation + /installation/language/config: + get: + description: get installation language config mapping + parameters: + - description: installation language + in: query + name: lang + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + summary: get installation language config mapping + tags: + - Lang + /installation/language/options: + get: + description: get installation language options + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/translator.LangOption' + type: array + type: object + summary: get installation language options + tags: + - Lang /personal/question/page: get: consumes: - application/json - description: UserList + description: list personal questions parameters: - default: string description: username @@ -3629,9 +7670,9 @@ paths: required: true type: string - default: "20" - description: pagesize + description: page_size in: query - name: pagesize + name: page_size required: true type: string produces: @@ -3643,9 +7684,22 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: UserList + summary: list personal questions + tags: + - Personal + /robots.txt: + get: + description: get site robots information + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + summary: get site robots information tags: - - api-question + - site securityDefinitions: ApiKeyAuth: in: header diff --git a/go.mod b/go.mod index 7fe0bfd9e..6578d5cef 100644 --- a/go.mod +++ b/go.mod @@ -1,93 +1,179 @@ -module github.com/answerdev/answer +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. -go 1.18 +module github.com/apache/answer + +go 1.23.0 require ( - github.com/Chain-Zhang/pinyin v0.1.3 + github.com/Machiel/slugify v1.0.1 + github.com/Masterminds/semver/v3 v3.3.0 github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/bwmarrin/snowflake v0.3.0 - github.com/gin-gonic/gin v1.8.1 - github.com/go-playground/locales v0.14.0 - github.com/go-playground/universal-translator v0.18.0 - github.com/go-playground/validator/v10 v10.11.1 - github.com/go-sql-driver/mysql v1.6.0 - github.com/goccy/go-json v0.9.11 - github.com/google/uuid v1.3.0 + github.com/disintegration/imaging v1.6.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.22.1 + github.com/go-sql-driver/mysql v1.8.1 + github.com/goccy/go-json v0.10.3 + github.com/google/uuid v1.6.0 github.com/google/wire v0.5.0 - github.com/jinzhu/copier v0.3.5 + github.com/grokify/html-strip-tags-go v0.1.0 + github.com/jinzhu/copier v0.4.0 github.com/jinzhu/now v1.1.5 - github.com/lib/pq v1.10.2 - github.com/mattn/go-sqlite3 v2.0.3+incompatible - github.com/mojocn/base64Captcha v1.3.5 - github.com/segmentfault/pacman v1.0.1 - github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 - github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.8.0 - github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a - github.com/swaggo/gin-swagger v1.5.3 - github.com/swaggo/swag v1.8.6 - golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be - golang.org/x/net v0.0.0-20220927171203-f486391704dc + github.com/lib/pq v1.10.9 + github.com/microcosm-cc/bluemonday v1.0.27 + github.com/mozillazg/go-pinyin v0.20.0 + github.com/ory/dockertest/v3 v3.11.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 + github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f + github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f + github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f + github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f + github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 + github.com/tidwall/gjson v1.17.3 + github.com/yuin/goldmark v1.7.4 + go.uber.org/mock v0.5.0 + golang.org/x/crypto v0.36.0 + golang.org/x/image v0.20.0 + golang.org/x/net v0.38.0 + golang.org/x/text v0.23.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df - xorm.io/builder v0.3.12 - xorm.io/core v0.7.3 + gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.33.0 + xorm.io/builder v0.3.13 xorm.io/xorm v1.3.2 ) require ( + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/andybalholm/brotli v1.0.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/bytedance/sonic v1.12.2 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.2.1+incompatible // indirect + github.com/docker/docker v27.2.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect + github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect + github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect + github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect + github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect + github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect + github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect + github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/spec v0.20.7 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/go-errors/errors v1.5.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-viper/mapstructure/v2 v2.1.0 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect - github.com/lestrrat-go/strftime v1.0.6 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/lestrrat-go/strftime v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.14 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/afero v1.9.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.13.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect + github.com/spf13/viper v1.19.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect - github.com/ugorji/go/codec v1.2.7 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect - golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.12 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/arch v0.10.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.25.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) + +replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1 + +replace modernc.org/cc/v3 v3.40.0 => gitlab.com/cznic/cc/v3 v3.40.0 diff --git a/go.sum b/go.sum index 238004c45..afe9b1ea9 100644 --- a/go.sum +++ b/go.sum @@ -1,221 +1,236 @@ 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 v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Chain-Zhang/pinyin v0.1.3 h1:RzErNyNwVa8z2sOLCuXSOtVdY/AsARb8mBzI2p2qtnE= -github.com/Chain-Zhang/pinyin v0.1.3/go.mod h1:5iHpt9p4znrnaP59/hfPMnAojajkDxQaP9io+tRMPho= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU= +github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM= +github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E= +github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o= 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= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 h1:vDHsaEcs/Q0dwetADENtwus6W1ccaZ9h3KBTm0d2X0g= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= -github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= +github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/docker/cli v27.2.1+incompatible h1:U5BPtiD0viUzjGAjV1p0MGB8eVA3L3cbIrnyWmSJI70= +github.com/docker/cli v27.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsoprea/go-exif v0.0.0-20190901173045-3ce78807c90f/go.mod h1:DmMpU91/Ax6BAwoRkjgRCr2rmgEgS4tsmatfV7M+U+c= +github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= +github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= +github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= +github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= +github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= +github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= +github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= +github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= +github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20190422055009-d6f9ba25cf48/go.mod h1:H1hAaFyv9cRV1ywoHvaqVoNSThBvWZ0JarRBcV+FSnE= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= +github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= +github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= +github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= +github.com/dsoprea/go-png-image-structure v0.0.0-20190624104353-c9b28dcdc5c8/go.mod h1:Bf0nmcDFFRQBjZwr9qY6c0zTxKQa+Q8YWZmlYxXGxY0= +github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA= +github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= +github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= +github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4= +github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= +github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -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= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 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/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +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/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 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.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= -github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +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/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +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.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +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/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190812012225-f41920e961ce/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= +github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 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= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -225,45 +240,32 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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.4.1/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.1/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.3/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -283,6 +285,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/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= @@ -292,10 +296,9 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -344,13 +347,15 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 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.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= 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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -359,50 +364,48 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +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/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= -github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= -github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= +github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -416,15 +419,14 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 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/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -435,6 +437,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -442,8 +448,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0= -github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY= +github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ= +github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -452,9 +458,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw= -github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -464,6 +469,12 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -472,33 +483,26 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -517,34 +521,42 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405 h1:2ieGkj4z/YPXVyQ2ayZUg3GwE1pYWd5f1RB6DzAOXKM= +github.com/scottleedavis/go-exif-remove v0.0.0-20230314195146-7e059d593405/go.mod h1:rIxVzVLKlBwLxO+lC+k/I4HJfRQcemg/f/76Xmmzsec= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE= -github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= -github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 h1:0xWBBXHHuemzMY61KYJXh7F5FW/4K8g98RYKNXodTCc= -github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= -github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 h1:WpnEbmZFE8FYIgvseX+NJtDgGJlM1KSaKJhoxJywUgo= -github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 h1:Q29Ky9ZUGhdLIygfX6jwPYeEa7Wqn8o3f1NJWb8LvvE= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= -github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 h1:7Adjc296AKv32dg88S0T8t9K3+N+PFYLSCctpPnCUr0= -github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= -github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 h1:CfuRhTPK2CBQIZruq5ceuTVthspe8U1FDjWXXI2RWdo= -github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f h1:9f2Bjf6bdMvNyUop32wAGJCdp+Jdm/d6nKBYvFvkRo0= +github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= +github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f h1:1KHe0uN6p798E7XJZPhZkgm/hXk5CTjisCvFMqaZSKI= +github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20230822083413-c0075a2d401f/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= +github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f h1:/nA4C3UfWw+3XYVBkgVMY1p3nX3uhl22hL2LW3FNcVs= +github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20230822083413-c0075a2d401f/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f h1:xia6AXJor4UV4T6htmHlfN7CGXZ04vlWwybVtFKJ/mA= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E= +github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f h1:0mrzVRrQ+mz5MWQSdC1y6dwKWiewYKkpRDqNf3nOhmk= +github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20230822083413-c0075a2d401f/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= +github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f h1:2gjiRmSj3J/F3A1A22UU1BzO4gQypEZx/4D7c7Ue4Ag= +github.com/segmentfault/pacman/contrib/server/http v0.0.0-20230822083413-c0075a2d401f/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -552,24 +564,26 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +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.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -577,79 +591,90 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= -github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= -github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= -github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= -github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= -github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +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/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/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.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.0/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/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +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.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +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/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= +golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -658,51 +683,28 @@ golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/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 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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/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= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -715,59 +717,34 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= -golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 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= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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= @@ -778,73 +755,47 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -852,60 +803,23 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +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/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -913,112 +827,31 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= @@ -1038,26 +871,20 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= @@ -1071,7 +898,6 @@ modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= -modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= @@ -1107,9 +933,12 @@ modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/E modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= -modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8= modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M= +modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= @@ -1145,37 +974,36 @@ modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= -modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE= modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= -modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE= modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= -modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= -modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= -xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0= -xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= +xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM= xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= diff --git a/i18n/af_ZA.yaml b/i18n/af_ZA.yaml new file mode 100644 index 000000000..f421ba9af --- /dev/null +++ b/i18n/af_ZA.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as <link> + head: + label: Head + text: This will insert before </head> + header: + label: Header + text: This will insert after <body> + footer: + label: Footer + text: This will insert before </body>. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/ar_SA.yaml b/i18n/ar_SA.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/ar_SA.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/az_AZ.yaml b/i18n/az_AZ.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/az_AZ.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/bal_BA.yaml b/i18n/bal_BA.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/bal_BA.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/ban_ID.yaml b/i18n/ban_ID.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/ban_ID.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/bn_BD.yaml b/i18n/bn_BD.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/bn_BD.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/bs_BA.yaml b/i18n/bs_BA.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/bs_BA.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/ca_ES.yaml b/i18n/ca_ES.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/ca_ES.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/cs_CZ.yaml b/i18n/cs_CZ.yaml new file mode 100644 index 000000000..2b691ec58 --- /dev/null +++ b/i18n/cs_CZ.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Úspěch. + unknown: + other: Neznámá chyba. + request_format_error: + other: Formát požadavku není platný. + unauthorized_error: + other: Neautorizováno. + database_error: + other: Chyba datového serveru. + forbidden_error: + other: Zakázáno. + duplicate_request_error: + other: Duplicitní odeslání. + action: + report: + other: Nahlásit + edit: + other: Upravit + delete: + other: Smazat + close: + other: Zavřít + reopen: + other: Znovu otevřít + forbidden_error: + other: Zakázáno. + pin: + other: Připnout + hide: + other: Skrýt + unpin: + other: Odepnout + show: + other: Zobrazit + invite_someone_to_answer: + other: Upravit + undelete: + other: Obnovit + merge: + other: Merge + role: + name: + user: + other: Uživatel + admin: + other: Administrátor + moderator: + other: Moderátor + description: + user: + other: Výchozí bez zvláštního přístupu. + admin: + other: Má plnou kontrolu nad stránkou. + moderator: + other: Má přístup ke všem příspěvkům kromě admin nastavení. + privilege: + level_1: + description: + other: Úroveň 1 (méně reputace je vyžadováno pro soukromý tým, skupinu) + level_2: + description: + other: Úroveň 2 (nízká reputace je vyžadována pro startovací komunitu) + level_3: + description: + other: Úroveň 3 (vysoká reputace je vyžadována pro vyspělou komunitu) + level_custom: + description: + other: Vlastní úroveň + rank_question_add_label: + other: Položit dotaz + rank_answer_add_label: + other: Napsat odpověď + rank_comment_add_label: + other: Napsat komentář + rank_report_add_label: + other: Nahlásit + rank_comment_vote_up_label: + other: Hlasovat pro komentář + rank_link_url_limit_label: + other: Zveřejnit více než 2 odkazy najednou + rank_question_vote_up_label: + other: Hlasovat pro dotaz + rank_answer_vote_up_label: + other: Hlasovat pro odpověď + rank_question_vote_down_label: + other: Hlasovat proti otázce + rank_answer_vote_down_label: + other: Hlasovat proti odpovědi + rank_invite_someone_to_answer_label: + other: Pozvěte někoho, aby odpověděl + rank_tag_add_label: + other: Vytvořit nový štítek + rank_tag_edit_label: + other: Upravit popis štítku (vyžaduje kontrolu) + rank_question_edit_label: + other: Upravit dotaz někoho jiného (vyžaduje kontrolu) + rank_answer_edit_label: + other: Upravit odpověď někoho jiného (vyžaduje kontrolu) + rank_question_edit_without_review_label: + other: Upravit dotaz někoho jiného (bez kontroly) + rank_answer_edit_without_review_label: + other: Upravit odpověď někoho jiného (bez kontroly) + rank_question_audit_label: + other: Zkontrolovat úpravy dotazu + rank_answer_audit_label: + other: Zkontrolovat úpravy odpovědí + rank_tag_audit_label: + other: Zkontrolovat úpravy štítků + rank_tag_edit_without_review_label: + other: Upravit popis štítku (bez kontroly) + rank_tag_synonym_label: + other: Správa synonym štítků + email: + other: Email + e_mail: + other: Email + password: + other: Heslo + pass: + other: Heslo + old_pass: + other: Current password + original_text: + other: Tento příspěvek + email_or_password_wrong_error: + other: Email a heslo nesouhlasí. + error: + common: + invalid_url: + other: Neplatná URL. + status_invalid: + other: Neplatný stav. + password: + space_invalid: + other: Heslo nesmí obsahovat mezery. + admin: + cannot_update_their_password: + other: Nemůžete změnit své heslo. + cannot_edit_their_profile: + other: Nemůžete upravovat svůj profil. + cannot_modify_self_status: + other: Nemůžete změnit svůj stav. + email_or_password_wrong: + other: Email a heslo nesouhlasí. + answer: + not_found: + other: Odpověď nebyla nalezena. + cannot_deleted: + other: Nemáte právo mazat. + cannot_update: + other: Nemáte právo aktualizovat. + question_closed_cannot_add: + other: Dotazy jsou uzavřené a není možno je přidávat. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Nejsou povoleny úpravy komentáře. + not_found: + other: Komentář nebyl nalezen. + cannot_edit_after_deadline: + other: Tento komentář byl pro úpravy příliš dlouhý. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email už existuje. + need_to_be_verified: + other: Email musí být ověřen. + verify_url_expired: + other: Platnost ověřovacího URL vypršela, pošlete si ověřovací email znovu. + illegal_email_domain_error: + other: Email z této domény není povolen. Použijte jinou doménu. + lang: + not_found: + other: Jazykový soubor nenalezen. + object: + captcha_verification_failed: + other: Nesprávně vyplněná Captcha. + disallow_follow: + other: Nemáte oprávnění sledovat. + disallow_vote: + other: Nemáte oprávnění hlasovat. + disallow_vote_your_self: + other: Nemůžete hlasovat pro svůj vlastní příspěvek. + not_found: + other: Objekt nenalezen. + verification_failed: + other: Ověření se nezdařilo. + email_or_password_incorrect: + other: Email a heslo nesouhlasí. + old_password_verification_failed: + other: Ověření starého hesla selhalo + new_password_same_as_previous_setting: + other: Nové heslo je stejné jako předchozí. + already_deleted: + other: Tento příspěvek byl odstraněn. + meta: + object_not_found: + other: Meta objekt nenalezen + question: + already_deleted: + other: Tento příspěvek byl odstraněn. + under_review: + other: Váš příspěvek čeká na kontrolu. Bude viditelný po jeho schválení. + not_found: + other: Dotaz nenalezen. + cannot_deleted: + other: Nemáte oprávnění k mazání. + cannot_close: + other: Nemáte oprávnění k uzavření. + cannot_update: + other: Nemáte oprávnění pro aktualizaci. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Hodnost reputace nesplňuje podmínku. + vote_fail_to_meet_the_condition: + other: Děkujeme za zpětnou vazbu. Potřebujete alespoň úroveň {{.Rank}}, abyste mohli hlasovat. + no_enough_rank_to_operate: + other: Potřebujete alespoň úroveň {{.Rank}} k provedení této akce. + report: + handle_failed: + other: Report selhal. + not_found: + other: Report nebyl nalezen. + tag: + already_exist: + other: Štítek již existuje. + not_found: + other: Štítek nebyl nalezen. + recommend_tag_not_found: + other: Doporučený štítek nebyl nalezen. + recommend_tag_enter: + other: Zadejte prosím alespoň jeden povinný štítek. + not_contain_synonym_tags: + other: Nemělo by obsahovat synonyma štítků. + cannot_update: + other: Nemáte oprávnění pro aktualizaci. + is_used_cannot_delete: + other: Nemůžete odstranit štítek, který se používá. + cannot_set_synonym_as_itself: + other: Aktuální štítek nelze jako synonymum stejného štítku. + smtp: + config_from_name_cannot_be_email: + other: Jméno odesílatele nemůže být emailová adresa. + theme: + not_found: + other: Motiv nebyl nalezen. + revision: + review_underway: + other: V současné době nelze upravit, čeká na kontrolu. + no_permission: + other: Nemáte oprávnění k revizi. + user: + external_login_missing_user_id: + other: Platforma třetí strany neposkytuje unikátní UserID, takže se nemůžete přihlásit, kontaktujte prosím správce webových stránek. + external_login_unbinding_forbidden: + other: Před odebráním tohoto typu přihlášení nastavte přihlašovací heslo pro svůj účet. + email_or_password_wrong: + other: + other: Email a heslo nesouhlasí. + not_found: + other: Uživatel nebyl nalezen. + suspended: + other: Uživatelský účet byl pozastaven. + username_invalid: + other: Uživatelské jméno je neplatné. + username_duplicate: + other: Uživatelské jméno je již použito. + set_avatar: + other: Nastavení avataru se nezdařilo. + cannot_update_your_role: + other: Nemůžete upravovat svoji roli. + not_allowed_registration: + other: Registrace nejsou povolené. + not_allowed_login_via_password: + other: Přihlášení přes heslo není povolené. + access_denied: + other: Přístup zamítnut + page_access_denied: + other: Nemáte přístup k této stránce. + add_bulk_users_format_error: + other: "Chyba formátu pole {{.Field}} poblíž '{{.Content}}' na řádku {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Počet uživatelů, které přidáte najednou, by měl být v rozsahu 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Načtení konfigurace selhalo + database: + connection_failed: + other: Spojení s databází selhalo + create_table_failed: + other: Vytvoření tabulky selhalo + install: + create_config_failed: + other: Soubor config.yaml nelze vytvořit. + upload: + unsupported_file_format: + other: Nepodporovaný formát souboru. + site_info: + config_not_found: + other: Konfigurace webu nebyla nalezena. + badge: + object_not_found: + other: Objekt odznaku nebyl nalezen + reason: + spam: + name: + other: spam + desc: + other: Tento příspěvek je reklama nebo vandalismus. Není užitečný ani relevantní pro aktuální téma. + rude_or_abusive: + name: + other: hrubý nebo zneužívající + desc: + other: "Rozumný člověk by tento obsah považoval za nevhodný pro slušnou konverzaci." + a_duplicate: + name: + other: duplicita + desc: + other: Tento dotaz byl položen dříve a již má odpověď. + placeholder: + other: Zadejte existující odkaz na dotaz + not_a_answer: + name: + other: není odpověď + desc: + other: "Toto bylo zveřejněno jako odpověď, ale nesnaží se odpovědět na dotaz. Měla by to být úprava, komentář, nebo úplně jiný dotaz." + no_longer_needed: + name: + other: již není potřeba + desc: + other: Tento komentář je zastaralý, konverzační nebo není relevantní pro tento příspěvek. + something: + name: + other: jiný důvod + desc: + other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše. + placeholder: + other: Dejte nám vědět konkrétně, v čem je problém + community_specific: + name: + other: důvod specifický pro komunitu + desc: + other: Tento dotaz nesplňuje pravidla komunity. + not_clarity: + name: + other: vyžaduje detaily nebo upřesnění + desc: + other: Tento dotaz v současné době obsahuje více otázek. Měl by se zaměřit pouze na jeden problém. + looks_ok: + name: + other: vypadá v pořádku + desc: + other: Tento příspěvek je dobrý tak jak je, nemá nízkou kvalitu. + needs_edit: + name: + other: potřebuje úpravu, kterou jsem udělal(a) + desc: + other: Zlepšete a opravte problémy s tímto příspěvkem. + needs_close: + name: + other: potřebuje zavřít + desc: + other: Na uzavřený dotaz není možné odpovídat, ale stále může být upraven a je možné pro něj hlasovat a komentovat jej. + needs_delete: + name: + other: potřebuje smazat + desc: + other: Tento příspěvek bude odstraněn. + question: + close: + duplicate: + name: + other: spam + desc: + other: Tento dotaz byl položena dříve a již má odpověď. + guideline: + name: + other: důvod specifický pro komunitu + desc: + other: Tento dotaz nesplňuje pravidla komunity. + multiple: + name: + other: vyžaduje detaily nebo upřesnění + desc: + other: Tento dotaz v současné době obsahuje více otázek. Měla by se zaměřit pouze na jeden problém. + other: + name: + other: jiný důvod + desc: + other: Tento příspěvek vyžaduje pozornost moderátorů z jiného důvodu, který není uveden výše. + operation_type: + asked: + other: dotázáno + answered: + other: zodpovězeno + modified: + other: upraveno + deleted_title: + other: Smazat dotaz + questions_title: + other: Dotazy + tag: + tags_title: + other: Štítky + no_description: + other: Štítek nemá žádný popis. + notification: + action: + update_question: + other: upravený dotaz + answer_the_question: + other: položil(a) dotaz + update_answer: + other: upravil(a) odpověď + accept_answer: + other: přijal(a) odpověď + comment_question: + other: okomentoval(a) dotaz + comment_answer: + other: okomentoval(a) odpověď + reply_to_you: + other: vám odpověděl(a) + mention_you: + other: vás zmínil(a) + your_question_is_closed: + other: Váš dotaz byl uzavřen + your_question_was_deleted: + other: Váš dotaz byl odstraněn + your_answer_was_deleted: + other: Vaše odpověď byla smazána + your_comment_was_deleted: + other: Váš komentář byl odstraněn + up_voted_question: + other: hlasoval(a) pro dotaz + down_voted_question: + other: hlasoval(a) proti dotazu + up_voted_answer: + other: hlasoval(a) pro odpověď + down_voted_answer: + other: hlasoval(a) proti odpovědi + up_voted_comment: + other: hlasoval(a) pro komentář + invited_you_to_answer: + other: vás pozval, abyste odpověděl(a) + earned_badge: + other: Získali jste odznak "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Potvrďte svůj nový email" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} odpověděl(a) na váš dotaz" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Obnova hesla" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Potvrďte svůj nový účet" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Zkušební email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: hlasovat pro + upvoted: + other: hlasováno pro + downvote: + other: hlasovat proti + downvoted: + other: hlasováno proti + accept: + other: přijmout + accepted: + other: přijato + edit: + other: upravit + review: + queued_post: + other: Příspěvek ve frontě + flagged_post: + other: Nahlášený příspěvek + suggested_post_edit: + other: Navrhované úpravy + reaction: + tooltip: + other: "{{ .Names }} a {{ .Count }} dalších..." + badge: + default_badges: + autobiographer: + name: + other: Životopisec + desc: + other: Profil vyplněn. + certified: + name: + other: Certifikovaný + desc: + other: Tutoriál pro nové uživatele dokončen. + editor: + name: + other: Editor + desc: + other: První úprava příspěvku. + first_flag: + name: + other: První nahlášení + desc: + other: První nahlášení příspěvku. + first_upvote: + name: + other: První hlas pro + desc: + other: První hlas pro příspěvek. + first_link: + name: + other: První odkaz + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: První sdílení + desc: + other: První sdílení příspěvku. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Napište 5 komentářů. + new_user_of_the_month: + name: + other: Nový uživatel měsíce + desc: + other: Výjimečný přínos ve svém prvním měsíci na stránce. + read_guidelines: + name: + other: Přečíst pravidla + desc: + other: Přečtěte si [pravidla komunity]. + reader: + name: + other: Čtenář + desc: + other: Přečtěte si všechny odpovědi v tématu s více než 10 odpověďmi. + welcome: + name: + other: Vítejte + desc: + other: Obdržel(a) hlas. + nice_share: + name: + other: Povedené sdílení + desc: + other: Sdílel(a) příspěvek s 25 unikátními návštěvníky. + good_share: + name: + other: Dobré sdílení + desc: + other: Sdílel(a) příspěvek s 300 unikátními návštěvníky. + great_share: + name: + other: Skvělé sdílení + desc: + other: Sdílel(a) příspěvek s 1000 unikátními návštěvníky. + out_of_love: + name: + other: Optimista + desc: + other: Využito 50 hlasů pro za den. + higher_love: + name: + other: Vytrvalý optimista + desc: + other: 5 krát využito 50 hlasů pro za den. + crazy_in_love: + name: + other: Bláznivý optimista + desc: + other: 20 krát využito 50 hlasů pro za den. + promoter: + name: + other: Promotér + desc: + other: Pozval(a) uživatele. + campaigner: + name: + other: Campaigner + desc: + other: Pozval(a) 3 uživatele. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Edit Tag + ask_a_question: Create Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + new_alerts: New alerts + all_read: Mark all as read + show_more: Show more + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Pozvěte další uživatele + desc: Pozvěte lidi, o kterých si myslíte, že mohou odpovědět. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Každý uživatel může napsat pouze jednu odpověď na stejný dotaz + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: URL základny Gravatar + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/cy_GB.yaml b/i18n/cy_GB.yaml new file mode 100644 index 000000000..3df4a38ac --- /dev/null +++ b/i18n/cy_GB.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Llwyddiant. + unknown: + other: Gwall anhysbys. + request_format_error: + other: Nid yw fformat y cais yn ddilys. + unauthorized_error: + other: Anawdurdodedig. + database_error: + other: Gwall gweinydd data. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Tynnu sylw + edit: + other: Golygu + delete: + other: Dileu + close: + other: Cau + reopen: + other: Ailagor + forbidden_error: + other: Forbidden. + pin: + other: Pinio + hide: + other: Dad-restru + unpin: + other: Dadbinio + show: + other: Rhestr + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: Defnyddiwr + admin: + other: Gweinyddwr + moderator: + other: Cymedrolwr + description: + user: + other: Diofyn heb unrhyw fynediad arbennig. + admin: + other: Bod â'r pŵer llawn i gael mynediad i'r safle. + moderator: + other: Mae ganddo fynediad i bob post ac eithrio gosodiadau gweinyddol. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: Ebost + e_mail: + other: Email + password: + other: Cyfrinair + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Nid yw e-bost a chyfrinair yn cyfateb. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: Ni allwch addasu eich cyfrinair. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: Ni allwch addasu eich statws. + email_or_password_wrong: + other: Nid yw e-bost a chyfrinair yn cyfateb. + answer: + not_found: + other: Ni cheir yr ateb. + cannot_deleted: + other: Dim caniatâd i ddileu. + cannot_update: + other: Dim caniatâd i ddiweddaru. + question_closed_cannot_add: + other: Mae cwestiynau ar gau ac ni ellir eu hychwanegu. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Nid oes modd golygu sylwadau. + not_found: + other: Sylw heb ei ganfod. + cannot_edit_after_deadline: + other: Mae'r amser sylwadau wedi bod yn rhy hir i'w addasu. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: E-bost yn bodoli eisoes. + need_to_be_verified: + other: Dylid gwirio e-bost. + verify_url_expired: + other: Mae'r URL wedi'i wirio gan e-bost wedi dod i ben, anfonwch yr e-bost eto. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Ffeil iaith heb ei chanfod. + object: + captcha_verification_failed: + other: Captcha anghywir. + disallow_follow: + other: Ni chaniateir i chi ddilyn. + disallow_vote: + other: Ni chaniateir i chi pleidleisio. + disallow_vote_your_self: + other: Ni allwch bleidleisio dros eich post eich hun. + not_found: + other: Heb ganfod y gwrthrych. + verification_failed: + other: Methodd y dilysu. + email_or_password_incorrect: + other: Nid yw e-bost a chyfrinair yn cyfateb. + old_password_verification_failed: + other: Methodd yr hen ddilysiad cyfrinair + new_password_same_as_previous_setting: + other: Mae'r cyfrinair newydd yr un fath â'r un blaenorol. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: Mae'r postiad hwn wedi'i ddileu. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Cwestiwn heb ei ganfod. + cannot_deleted: + other: Dim caniatâd i ddileu. + cannot_close: + other: Dim caniatâd i cau. + cannot_update: + other: Dim caniatâd i ddiweddaru. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Methodd handlen yr adroddiad. + not_found: + other: Heb ganfod yr adroddiad. + tag: + already_exist: + other: Mae tag eisoes yn bodoli. + not_found: + other: Tag heb ei ddarganfod. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Rhowch o leiaf un tag gofynnol. + not_contain_synonym_tags: + other: Ni ddylai gynnwys tagiau cyfystyr. + cannot_update: + other: Dim caniatâd i ddiweddaru. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: Ni allwch osod cyfystyr y tag cyfredol fel ei hun. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Thema heb ei ddarganfod. + revision: + review_underway: + other: Methu â golygu ar hyn o bryd, mae fersiwn yn y ciw adolygu. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Nid yw e-bost a chyfrinair yn cyfateb. + not_found: + other: Defnyddwr heb ei ddarganfod. + suspended: + other: Mae'r defnyddiwr hwn wedi'i atal. + username_invalid: + other: Mae'r enw defnyddiwr yn annilys. + username_duplicate: + other: Cymerwyd yr enw defnyddiwr eisoes. + set_avatar: + other: Methodd set avatar. + cannot_update_your_role: + other: Ni allwch addasu eich rôl. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Wedi methu darllen y ffurfwedd + database: + connection_failed: + other: Methodd cysylltiad cronfa ddata + create_table_failed: + other: Methwyd creu tabl + install: + create_config_failed: + other: Methu creu'r ffeil config.yaml. + upload: + unsupported_file_format: + other: Fformat ffeil heb ei gefnogi. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: sbam + desc: + other: Mae'r cwestiwn hwn wedi'i ofyn o'r blaen ac mae ganddo ateb yn barod. + guideline: + name: + other: rheswm cymunedol-benodol + desc: + other: Nid yw'r cwestiwn hwn yn bodloni canllaw cymunedol. + multiple: + name: + other: angen manylion neu eglurder + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: rhywbeth arall + desc: + other: Mae'r swydd hon angen reswm arall nad yw wedi'i restru uchod. + operation_type: + asked: + other: gofynnodd + answered: + other: atebodd + modified: + other: wedi newid + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: cwestiwn wedi'i ddiweddaru + answer_the_question: + other: cwestiwn wedi ei ateb + update_answer: + other: ateb wedi'i ddiweddaru + accept_answer: + other: ateb derbyniol + comment_question: + other: cwestiwn a wnaed + comment_answer: + other: ateb a wnaed + reply_to_you: + other: atebodd i chi + mention_you: + other: wedi sôn amdanoch + your_question_is_closed: + other: Mae eich cwestiwn wedi’i gau + your_question_was_deleted: + other: Mae eich cwestiwn wedi’i dileu + your_answer_was_deleted: + other: Mae eich ateb wedi’i dileu + your_comment_was_deleted: + other: Mae eich sylw wedi’i dileu + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Sut i Fformatio + desc: >- + + pagination: + prev: Cynt + next: Nesaf + page_title: + question: Cwestiwn + questions: Cwestiynau + tag: Tag + tags: Tagiau + tag_wiki: tag wiki + create_tag: Creu Tag + edit_tag: Golygu Tag + ask_a_question: Create Question + edit_question: Golygu Cwestiwn + edit_answer: Golygu Ateb + search: Chwiliwch + posts_containing: Postiadau yn cynnwys + settings: Gosodiadau + notifications: Hysbysiadau + login: Mewngofnodi + sign_up: Cofrestru + account_recovery: Adfer Cyfrif + account_activation: Ysgogi Cyfrif + confirm_email: Cadarnhau e-bost + account_suspended: Cyfrif wedi'i atal + admin: Gweinyddu + change_email: Addasu E-bost + install: Ateb Gosod + upgrade: Ateb Uwchraddio + maintenance: Cynnal a Chadw Gwefan + users: Defnyddwyr + oauth_callback: Processing + http_404: Gwall HTTP 404 + http_50X: Gwall HTTP 500 + http_403: Gwall HTTP 403 + logout: Log Out + notifications: + title: Hysbysiadau + inbox: Mewnflwch + achievement: Llwyddiannau + new_alerts: New alerts + all_read: Marciwch y cyfan fel wedi'i ddarllen + show_more: Dangos mwy + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Mae'ch Cyfrif wedi'i Atal + until_time: "Cafodd eich cyfrif ei atal tan {{ time }}." + forever: Cafodd y defnyddiwr hwn ei atal am byth. + end: Nid ydych yn arwain cymunedol. + contact_us: Contact us + editor: + blockquote: + text: Dyfyniad + bold: + text: Cryf + chart: + text: Siart + flow_chart: Siart llif + sequence_diagram: Diagram dilyniant + class_diagram: Diagram dosbarth + state_diagram: Diagram cyflwr + entity_relationship_diagram: Diagram perthynas endid + user_defined_diagram: Diagram wedi'i ddiffinio gan y defnyddiwr + gantt_chart: Siart Gantt + pie_chart: Siart cylch + code: + text: Sampl côd + add_code: Ychwanegu sampl côd + form: + fields: + code: + label: Côd + msg: + empty: Ni all côd fod yn wag. + language: + label: Iaith + placeholder: Synhwyriad awtomatig + btn_cancel: Canslo + btn_confirm: Ychwanegu + formula: + text: Fformiwla + options: + inline: Fformiwla mewn-lein + block: Fformiwla bloc + heading: + text: Pennawd + options: + h1: Pennawd 1 + h2: Pennawd 2 + h3: Pennawd 3 + h4: Pennawd 4 + h5: Pennawd 5 + h6: Pennawd 6 + help: + text: Cymorth + hr: + text: Horizontal rule + image: + text: Delwedd + add_image: Ychwanegu delwedd + tab_image: Uwchlwytho delwedd + form_image: + fields: + file: + label: Image file + btn: Dewis delwedd + msg: + empty: Ni all ffeil fod yn wag. + only_image: Dim ond ffeiliau delwedd a ganiateir. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Disgrifiad + tab_url: URL delwedd + form_url: + fields: + url: + label: URL delwedd + msg: + empty: Ni all URL delwedd fod yn wag. + name: + label: Disgrifiad + btn_cancel: Canslo + btn_confirm: Ychwanegu + uploading: Wrthi'n uwchlwytho + indent: + text: Mewnoliad + outdent: + text: Alloliad + italic: + text: Pwyslais + link: + text: Hypergyswllt + add_link: Ychwanegu hypergyswllt + form: + fields: + url: + label: URL + msg: + empty: Ni all URL fod yn wag. + name: + label: Disgrifiad + btn_cancel: Canslo + btn_confirm: Ychwanegu + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Tabl + heading: Pennawd + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Rwy'n cau'r post hon fel... + btn_cancel: Canslo + btn_submit: Cyflwyno + remark: + empty: Ni all fod yn wag. + msg: + empty: Dewis rheswm. + report_modal: + flag_title: Dwi'n tynnu sylw i adrodd y swydd hon fel... + close_title: Rwy'n cau'r post hon fel... + review_question_title: Adolygu cwestiwn + review_answer_title: Adolygu ateb + review_comment_title: Adolygu sylwad + btn_cancel: Canslo + btn_submit: Cyflwyno + remark: + empty: Ni all fod yn wag. + msg: + empty: Dewis rheswm. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Creu tag newydd + form: + fields: + display_name: + label: Display name + msg: + empty: Ni all fod enw dangos yn wag. + range: Enw arddangos hyd at 35 nod. + slug_name: + label: URL slug + desc: Slug URL hyd at 35 nod. + msg: + empty: Ni all Slug URL fod yn wag. + range: Slug URL hyd at 35 nod. + character: Mae slug URL yn cynnwys set nodau na caniateir. + desc: + label: Disgrifiad + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Canslo + btn_submit: Cyflwyno + btn_post: Post tag newydd + tag_info: + created_at: Creuwyd + edited_at: Golygwyd + history: Hanes + synonyms: + title: Cyfystyron + text: Bydd y tagiau canlynol yn cael eu hail-fapio i + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Canslo + tags: + title: Tagiau + sort_buttons: + popular: Poblogaidd + name: Enw + newest: Newest + button_follow: Dilyn + button_following: Yn dilyn + tag_label: cwestiynau + search_placeholder: Hidlo yn ôl enw tag + no_desc: Nid oes gan y tag unrhyw ddisgrifiad. + more: Mwy + wiki: Wiki + ask: + title: Create Question + edit_title: Golygu Cwestiwn + default_reason: Golygu Cwestiwn + default_first_reason: Create question + similar_questions: Cwestiynau tebyg + form: + fields: + revision: + label: Diwygiad + title: + label: Teitl + placeholder: What's your topic? Be specific. + msg: + empty: Ni all teitl fod yn wag. + range: Teitl hyd at 20 nod + body: + label: Corff + msg: + empty: Ni all corff fod yn wag. + tags: + label: Tagiau + msg: + empty: Ni all tagiau fod yn wag. + answer: + label: Ateb + msg: + empty: Ni all ateb fod yn wag. + edit_summary: + label: Edit summary + placeholder: >- + Eglurwch yn fyr eich newidiadau (sillafu wedi'i gywiro, gramadeg sefydlog, fformatio gwell) + btn_post_question: Post cweistiwn + btn_save_edits: Cadw golygiadau + answer_question: Atebwch eich cwestiwn eich hun + post_question&answer: Postiwch eich cwestiwn ac ateb + tag_selector: + add_btn: Ychwanegu tag + create_btn: Creu tag newydd + search_tag: Chwilio tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar Base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/da_DK.yaml b/i18n/da_DK.yaml new file mode 100644 index 000000000..2d1b7a2ea --- /dev/null +++ b/i18n/da_DK.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Gennemført. + unknown: + other: Ukendt fejl. + request_format_error: + other: Forespørgselsformat er ikke gyldigt. + unauthorized_error: + other: Uautoriseret. + database_error: + other: Data-server fejl. + forbidden_error: + other: Forbudt. + duplicate_request_error: + other: Duplilkeret indenselse. + action: + report: + other: Anmeld + edit: + other: Rediger + delete: + other: Slet + close: + other: Luk + reopen: + other: Genåbn + forbidden_error: + other: Forbudt. + pin: + other: Fastgør + hide: + other: Afliste + unpin: + other: Frigør + show: + other: Liste + invite_someone_to_answer: + other: Rediger + undelete: + other: Genopret + merge: + other: Merge + role: + name: + user: + other: Bruger + admin: + other: Administrator + moderator: + other: Moderator + description: + user: + other: Standard uden særlig adgang. + admin: + other: Hav den fulde magt til at få adgang til webstedet. + moderator: + other: Har adgang til alle indlæg undtagen administratorindstillinger. + privilege: + level_1: + description: + other: Niveau 1 (mindre omdømme kræves for private team, gruppe) + level_2: + description: + other: Niveau 2 (lav omdømme kræves for opstart fællesskab) + level_3: + description: + other: Niveau 3 (højt omdømme kræves for moden fællesskab) + level_custom: + description: + other: Brugerdefineret Niveau + rank_question_add_label: + other: Stil spørgsmål + rank_answer_add_label: + other: Skriv svar + rank_comment_add_label: + other: Skriv kommentar + rank_report_add_label: + other: Anmeld + rank_comment_vote_up_label: + other: Op-stem kommentar + rank_link_url_limit_label: + other: Skriv mere end 2 links ad gangen + rank_question_vote_up_label: + other: Op-stem spørgsmål + rank_answer_vote_up_label: + other: Op-stem svar + rank_question_vote_down_label: + other: Ned-stem spørgsmål + rank_answer_vote_down_label: + other: Ned-stem svar + rank_invite_someone_to_answer_label: + other: Inviter nogen til at svare + rank_tag_add_label: + other: Opret et nyt tag + rank_tag_edit_label: + other: Rediger tag beskrivelse (skal gennemgås) + rank_question_edit_label: + other: Rediger andres spørgsmål (skal gennemgås) + rank_answer_edit_label: + other: Redigere andres svar (skal gennemgås) + rank_question_edit_without_review_label: + other: Rediger andres spørgsmål uden gennemgang + rank_answer_edit_without_review_label: + other: Rediger andres svar uden gennemgang + rank_question_audit_label: + other: Gennemse spørgsmål redigeringer + rank_answer_audit_label: + other: Gennemgå svar redigeringer + rank_tag_audit_label: + other: Gennemse tag redigeringer + rank_tag_edit_without_review_label: + other: Rediger tag beskrivelse uden gennemgang + rank_tag_synonym_label: + other: Administrer tag synonymer + email: + other: E-mail + e_mail: + other: E-mail + password: + other: Adgangskode + pass: + other: Adgangskode + old_pass: + other: Current password + original_text: + other: Dette indlæg + email_or_password_wrong_error: + other: E-mail og adgangskode stemmer ikke overens. + error: + common: + invalid_url: + other: Ugyldig URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Adgangskoden må ikke indeholde mellemrum. + admin: + cannot_update_their_password: + other: Du kan ikke ændre din adgangskode. + cannot_edit_their_profile: + other: Du kan ikke ændre din profil. + cannot_modify_self_status: + other: Du kan ikke ændre din status. + email_or_password_wrong: + other: E-mail og adgangskode stemmer ikke overens. + answer: + not_found: + other: Svar ikke fundet. + cannot_deleted: + other: Ingen tilladelser til at slette. + cannot_update: + other: Ingen tilladelse til at opdatere. + question_closed_cannot_add: + other: Spørgsmål er lukket og kan ikke tilføjes. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Kommentar er ikke tilladt at redigere. + not_found: + other: Kommentar ikke fundet. + cannot_edit_after_deadline: + other: Kommentaren er for gammel til at blive redigeret. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email eksisterer allerede. + need_to_be_verified: + other: E-mail skal bekræftes. + verify_url_expired: + other: Email bekræftet URL er udløbet. Send venligst e-mailen igen. + illegal_email_domain_error: + other: E-mail er ikke tilladt fra dette e-mail-domæne. Brug venligst et andet. + lang: + not_found: + other: Sprog-fil kunne ikke findes. + object: + captcha_verification_failed: + other: Captcha er forkert. + disallow_follow: + other: Du har ikke tilladelse til at følge. + disallow_vote: + other: Du har ikke tilladelse til at stemme. + disallow_vote_your_self: + other: Du kan ikke stemme på dit eget indlæg. + not_found: + other: Objekt ikke fundet. + verification_failed: + other: Verifikation mislykkedes. + email_or_password_incorrect: + other: E-mail og adgangskode stemmer ikke overens. + old_password_verification_failed: + other: Den gamle adgangskodebekræftelse mislykkedes + new_password_same_as_previous_setting: + other: Den nye adgangskode er den samme som den foregående. + already_deleted: + other: Dette indlæg er blevet slettet. + meta: + object_not_found: + other: Metaobjekt ikke fundet + question: + already_deleted: + other: Dette indlæg er blevet slettet. + under_review: + other: Dit indlæg afventer gennemgang. Det vil være synligt, når det er blevet godkendt. + not_found: + other: Spørgsmål ikke fundet. + cannot_deleted: + other: Ingen tilladelser til at slette. + cannot_close: + other: Ingen tilladelse til at lukke. + cannot_update: + other: Ingen tilladelse til at opdatere. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Omdømmelse rang opfylder ikke betingelsen. + vote_fail_to_meet_the_condition: + other: Tak for feedback. Du skal mindst have {{.Rank}} ry for at afgive en stemme. + no_enough_rank_to_operate: + other: Du skal mindst {{.Rank}} omdømme for at gøre dette. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Rapport ikke fundet. + tag: + already_exist: + other: Tag findes allerede. + not_found: + other: Tag blev ikke fundet. + recommend_tag_not_found: + other: Anbefal tag eksisterer ikke. + recommend_tag_enter: + other: Indtast mindst et påkrævet tag. + not_contain_synonym_tags: + other: Må ikke indeholde synonym tags. + cannot_update: + other: Ingen tilladelse til at opdatere. + is_used_cannot_delete: + other: Du kan ikke slette et tag, der er i brug. + cannot_set_synonym_as_itself: + other: Du kan ikke indstille synonymet for det nuværende tag som sig selv. + smtp: + config_from_name_cannot_be_email: + other: Fra-navnet kan ikke være en e-mail-adresse. + theme: + not_found: + other: Tema ikke fundet. + revision: + review_underway: + other: Kan ikke redigere i øjeblikket, der er en version i revisionskøen. + no_permission: + other: Ingen tilladelse til at revidere. + user: + external_login_missing_user_id: + other: Den tredjepartsplatform giver ikke et unikt UserID, så du kan ikke logge ind, kontakt venligst webstedsadministratoren. + external_login_unbinding_forbidden: + other: Angiv en adgangskode til din konto, før du fjerner dette login. + email_or_password_wrong: + other: + other: E-mail og adgangskode stemmer ikke overens. + not_found: + other: Bruger ikke fundet. + suspended: + other: Brugeren er suspenderet. + username_invalid: + other: Brugernavn er ugyldigt. + username_duplicate: + other: Brugernavn er allerede i brug. + set_avatar: + other: Avatar sæt mislykkedes. + cannot_update_your_role: + other: Du kan ikke ændre din rolle. + not_allowed_registration: + other: Webstedet er ikke åbent for registrering. + not_allowed_login_via_password: + other: I øjeblikket er det ikke tilladt at logge ind via adgangskode. + access_denied: + other: Adgang nægtet + page_access_denied: + other: Du har ikke adgang til denne side. + add_bulk_users_format_error: + other: "Fejl {{.Field}} format nær '{{.Content}}' i linje {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Antallet af brugere du tilføjer på én gang skal være i intervallet 1 -{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Kunne ikke læse konfigurationen + database: + connection_failed: + other: Database forbindelse mislykkedes + create_table_failed: + other: Tabellen kunne ikke oprettes + install: + create_config_failed: + other: Kan ikke oprette filen config.yaml. + upload: + unsupported_file_format: + other: Ikke understøttet filformat. + site_info: + config_not_found: + other: Site config ikke fundet. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: Dette indlæg er en annonce eller vandalisme. Det er ikke nyttigt eller relevant for det aktuelle emne. + rude_or_abusive: + name: + other: uhøflig eller misbrug + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: en duplikering + desc: + other: Dette spørgsmål er blevet stillet før og har allerede et svar. + placeholder: + other: Indtast linket til eksisterende spørgsmål + not_a_answer: + name: + other: ikke et svar + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: ikke længere nødvendigt + desc: + other: Denne kommentar er forældet, samtale-agtig eller ikke relevant for dette indlæg. + something: + name: + other: noget andet + desc: + other: Dette indlæg kræver personalets opmærksomhed af en anden grund, som ikke er nævnt ovenfor. + placeholder: + other: Lad os vide specifikt, hvad du er bekymret over + community_specific: + name: + other: en fællesskabsspecifik årsag + desc: + other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje. + not_clarity: + name: + other: kræver detaljer eller klarhed + desc: + other: Dette spørgsmål indeholder i øjeblikket flere spørgsmål i én. Det bør kun fokusere på ét problem. + looks_ok: + name: + other: ser OK ud + desc: + other: Dette indlæg er godt som er og ikke lav kvalitet. + needs_edit: + name: + other: har brug for redigering, og jeg gjorde det + desc: + other: Forbedre og ret selv problemer med dette indlæg. + needs_close: + name: + other: skal lukkes + desc: + other: Et lukket spørgsmål kan ikke besvares, men du kan stadig redigere, stemme og kommentere. + needs_delete: + name: + other: skal slettes + desc: + other: Dette indlæg bliver slettet. + question: + close: + duplicate: + name: + other: spam + desc: + other: Dette spørgsmål er blevet stillet før og har allerede et svar. + guideline: + name: + other: en fællesskabsspecifik årsag + desc: + other: Dette spørgsmål opfylder ikke en fællesskabsretningslinje. + multiple: + name: + other: kræver detaljer eller klarhed + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: noget andet + desc: + other: Dette indlæg kræver en anden grund som ikke er nævnt ovenfor. + operation_type: + asked: + other: spurgt + answered: + other: besvaret + modified: + other: ændret + deleted_title: + other: Slettet spørgsmål + questions_title: + other: Spørgsmål + tag: + tags_title: + other: Tags + no_description: + other: Tag har ingen beskrivelse. + notification: + action: + update_question: + other: opdateret spørgsmål + answer_the_question: + other: besvaret spørgsmål + update_answer: + other: opdateret svar + accept_answer: + other: accepteret svar + comment_question: + other: kommenteret spørgsmål + comment_answer: + other: kommenteret svar + reply_to_you: + other: svarede dig + mention_you: + other: nævnte dig + your_question_is_closed: + other: Dit spørgsmål er blevet lukket + your_question_was_deleted: + other: Dit spørgsmål er blevet slettet + your_answer_was_deleted: + other: Dit svar er blevet slettet + your_comment_was_deleted: + other: Din kommentar er slettet + up_voted_question: + other: op-stemt spørgsmål + down_voted_question: + other: ned-stemt spørgsmål + up_voted_answer: + other: op-stemt svar + down_voted_answer: + other: ned-stemt svar + up_voted_comment: + other: op-stemt kommentar + invited_you_to_answer: + other: inviterede dig til at svare + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Bekræft din nye e-mailadresse" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} besvarede dit spørgsmål" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} inviterede dig til at svare" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} kommenterede dit indlæg" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] Nyt spørgsmål: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Nulstilling af adgangskode" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Bekræft din nye konto" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test E-Mail" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: stem op + upvoted: + other: stemt op + downvote: + other: stem ned + downvoted: + other: stemt ned + accept: + other: acceptér + accepted: + other: accepteret + edit: + other: rediger + review: + queued_post: + other: Indlæg i kø + flagged_post: + other: Anmeldt indlæg + suggested_post_edit: + other: Foreslåede redigeringer + reaction: + tooltip: + other: "{{ .Names }} og {{ .Count }} mere..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Sådan formaterer du + desc: >- + + pagination: + prev: Forrige + next: Næste + page_title: + question: Spørgsmål + questions: Spørgsmål + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Opret tag + edit_tag: Rediger tag + ask_a_question: Create Question + edit_question: Rediger spørgsmål + edit_answer: Rediger Svar + search: Søg + posts_containing: Indlæg som indeholder + settings: Indstillinger + notifications: Notifikationer + login: Log Ind + sign_up: Tilmeld dig + account_recovery: Konto-gendannelse + account_activation: Aktivering af konto + confirm_email: Bekræft e-mail + account_suspended: Konto suspenderet + admin: Administrator + change_email: Ændre E-Mail + install: Answer Installation + upgrade: Answer Opgradering + maintenance: Vedligeholdelse af websted + users: Brugere + oauth_callback: Behandler + http_404: HTTP Fejl 404 + http_50X: Http Fejl 500 + http_403: HTTP Fejl 403 + logout: Log Ud + notifications: + title: Notifikationer + inbox: Indbakke + achievement: Bedrifter + new_alerts: Nye adviseringer + all_read: Markér alle som læst + show_more: Vis mere + someone: Nogen + inbox_type: + all: Alle + posts: Indlæg + invites: Invitationer + votes: Stemmer + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Din konto er blevet suspenderet + until_time: "Din konto blev suspenderet indtil {{ time }}." + forever: Denne bruger blev suspenderet for evigt. + end: Du opfylder ikke en fællesskabsretningslinje. + contact_us: Kontakt os + editor: + blockquote: + text: Citatblok + bold: + text: Fed + chart: + text: Diagram + flow_chart: Flow- diagram + sequence_diagram: Sekvensdiagram + class_diagram: Klassediagram + state_diagram: Tilstands-diagram + entity_relationship_diagram: Enheds-forhold-diagram + user_defined_diagram: Brugerdefineret diagram + gantt_chart: Gantt- diagram + pie_chart: Cirkeldiagram + code: + text: Kode-eksempel + add_code: Tilføj kodeeksempel + form: + fields: + code: + label: Kode + msg: + empty: Kode skal udfyldes. + language: + label: Sprog + placeholder: Automatisk detektering + btn_cancel: Annuller + btn_confirm: Tilføj + formula: + text: Formel + options: + inline: Indlejret formel + block: Formel blok + heading: + text: Overskrift + options: + h1: Overskrift 1 + h2: Overskrift 2 + h3: Overskrift 3 + h4: Overskrift 4 + h5: Overskrift 5 + h6: Overskrift 6 + help: + text: Hjælp + hr: + text: Vandret streg + image: + text: Billede + add_image: Tilføj billede + tab_image: Upload billede + form_image: + fields: + file: + label: Billedfil + btn: Vælg billede + msg: + empty: Filen skal udfyldes. + only_image: Kun billedfiler er tilladt. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Beskriveslse + tab_url: Billede-URL + form_url: + fields: + url: + label: Billede-URL + msg: + empty: Billede-URL skal udfyldes. + name: + label: Beskriveslse + btn_cancel: Annuller + btn_confirm: Tilføj + uploading: Uploader + indent: + text: Indrykning + outdent: + text: Udrykning + italic: + text: Fremhævning + link: + text: Link + add_link: Tilføj link + form: + fields: + url: + label: URL + msg: + empty: URL må ikke være tom. + name: + label: Beskriveslse + btn_cancel: Annuller + btn_confirm: Tilføj + ordered_list: + text: Nummereret liste + unordered_list: + text: Punktliste + table: + text: Tabel + heading: Overskrift + cell: Celle + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Jeg lukker dette indlæg fordi... + btn_cancel: Annuller + btn_submit: Indsend + remark: + empty: skal udfyldes. + msg: + empty: Vælg en grund. + report_modal: + flag_title: Jeg markerer for at rapportere dette indlæg som... + close_title: Jeg lukker dette indlæg fordi... + review_question_title: Gennemgå spørgsmål + review_answer_title: Gennemgå svar + review_comment_title: Gennemgå kommentar + btn_cancel: Annuller + btn_submit: Indsend + remark: + empty: skal udfyldes. + msg: + empty: Vælg en grund. + not_a_url: URL-format er forkert. + url_not_match: URL oprindelsen matcher ikke det aktuelle websted. + tag_modal: + title: Opret et nyt tag + form: + fields: + display_name: + label: Visnings-navn + msg: + empty: Visnings-navn skal udfyldes. + range: Visnings-navn på op til 35 tegn. + slug_name: + label: URL-slug + desc: URL slug op til 35 tegn. + msg: + empty: URL slug må ikke være tom. + range: URL slug op til 35 tegn. + character: URL slug indeholder ikke tilladte tegn. + desc: + label: Beskriveslse + revision: + label: Revision + edit_summary: + label: Rediger resumé + placeholder: >- + Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) + btn_cancel: Annuller + btn_submit: Indsend + btn_post: Send nyt tag + tag_info: + created_at: Oprettet + edited_at: Redigeret + history: Historik + synonyms: + title: Synonymer + text: Følgende tags vil blive genmappet til + empty: Ingen synonymer fundet. + btn_add: Tilføj et synonym + btn_edit: Rediger + btn_save: Gem + synonyms_text: Følgende tags vil blive genmappet til + delete: + title: Slet dette tag + tip_with_posts: >- +

Vi tillader ikke at slette tag med indlæg.

Fjern venligst dette tag fra indlæggene først.

+ tip_with_synonyms: >- +

Vi tillader ikke at slette tag med indlæg.

Fjern venligst dette tag fra indlæggene først.

+ tip: Er du sikker på, at du vil slette? + close: Luk + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Rediger tag + default_reason: Rediger tag + default_first_reason: Tilføj tag + btn_save_edits: Gem ændringer + btn_cancel: Annuller + dates: + long_date: MMM D + long_date_with_year: "D MMMM, YYYY" + long_date_with_time: "MMM D, ÅÅÅÅ [at] HH:mm" + now: nu + x_seconds_ago: "{{count}}s siden" + x_minutes_ago: "{{count}}s siden" + x_hours_ago: "{{count}}t siden" + hour: time + day: dag + hours: timer + days: dag + month: month + months: months + year: year + reaction: + heart: hjerte + smile: smil + frown: rynke panden + btn_label: tilføj eller fjern reaktioner + undo_emoji: fortryd {{ emoji }} reaktion + react_emoji: reager med {{ emoji }} + unreact_emoji: ikke reager med {{ emoji }} + comment: + btn_add_comment: Tilføj kommentar + reply_to: Svar til + btn_reply: Svar + btn_edit: Rediger + btn_delete: Slet + btn_flag: Anmeld + btn_save_edits: Gem ændringer + btn_cancel: Annuller + show_more: "{{count}} flere kommentarer" + tip_question: >- + Brug kommentarer til at bede om mere information eller foreslå forbedringer. Undgå at besvare spørgsmål i kommentarer. + tip_answer: >- + Brug kommentarer til at svare andre brugere eller give dem besked om ændringer. Hvis du tilføjer nye oplysninger, skal du redigere dit indlæg i stedet for at kommentere. + tip_vote: Det tilføjer noget nyttigt til indlægget + edit_answer: + title: Rediger Svar + default_reason: Rediger svar + default_first_reason: Tilføj svar + form: + fields: + revision: + label: Revision + answer: + label: Svar + feedback: + characters: indhold skal være mindst 6 tegn. + edit_summary: + label: Rediger resumé + placeholder: >- + Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) + btn_save_edits: Gem ændringer + btn_cancel: Annuller + tags: + title: Tags + sort_buttons: + popular: Populære + name: Navn + newest: Nyeste + button_follow: Følg + button_following: Følger + tag_label: spørgsmål + search_placeholder: Filtrer efter tag-navn + no_desc: Tag har ingen beskrivelse. + more: Mere + wiki: Wiki + ask: + title: Create Question + edit_title: Rediger spørgsmål + default_reason: Rediger spørgsmål + default_first_reason: Create question + similar_questions: Lignende spørgsmål + form: + fields: + revision: + label: Revision + title: + label: Titel + placeholder: What's your topic? Be specific. + msg: + empty: Titel må ikke være tom. + range: Titel på op til 150 tegn + body: + label: Brødtekst + msg: + empty: Brødtekst skal udfyldes. + tags: + label: Tags + msg: + empty: Tags må ikke være tom. + answer: + label: Svar + msg: + empty: Svar må ikke være tomt. + edit_summary: + label: Rediger resumé + placeholder: >- + Forklar kort dine ændringer (korrigeret stavning, fast grammatik, forbedret formatering) + btn_post_question: Indsend dit spørgsmål + btn_save_edits: Gem ændringer + answer_question: Besvar dit eget spørgsmål + post_question&answer: Send dit spørgsmål og svar + tag_selector: + add_btn: Tilføj tag + create_btn: Opret et nyt tag + search_tag: Søg tag + hint: "Describe what your content is about, at least one tag is required." + no_result: Ingen tags matchede + tag_required_text: Påkrævet tag (mindst én) + header: + nav: + question: Spørgsmål + tag: Tags + user: Brugere + badges: Badges + profile: Profil + setting: Indstillinger + logout: Log Ud + admin: Administrator + review: Gennemgå + bookmark: Bogmærker + moderation: Moderering + search: + placeholder: Søg + footer: + build_on: >- + Drevet af <1> Apache Answer- open source-softwaren, der driver Q&A-fællesskaber.
Fremstillet med kærlighed ©️ {{cc}}. + upload_img: + name: Skift + loading: indlæser... + pic_auth_code: + title: Captcha + placeholder: Skriv teksten ovenfor + msg: + empty: Captcha må ikke være tomt. + inactive: + first: >- + Du er næsten færdig! Vi har sendt en aktiveringsmail til {{mail}}. Følg venligst instruktionerne i mailen for at aktivere din konto. + info: "Hvis det ikke ankommer, tjek din spam-mappe." + another: >- + Vi har sendt endnu en aktiverings-e-mail til dig på {{mail}}. Det kan tage nogen få minutter før den når frem; kontrollér også din spam-mappe. + btn_name: Send aktiverings-e-mail igen + change_btn_name: Ændre e-mail + msg: + empty: skal udfyldes. + resend_email: + url_label: Er du sikker på, at du vil sende aktiveringse-mailen? + url_text: Du kan også give aktiveringslinket ovenfor til brugeren. + login: + login_to_continue: Log ind for at fortsætte + info_sign: Har du ikke en konto? <1>Tilmeld dig + info_login: Har du allerede en konto? <1>Log ind + agreements: Ved at registrere dig accepterer du <1>privacy policy og <3>terms of service . + forgot_pass: Glemt adgangskode? + name: + label: Navn + msg: + empty: Navn må ikke være tomt. + range: Name must be between 2 to 30 characters in length. + character: 'Skal bruge tegnsættet "a-z", "A-Z", "0-9", " - . _"' + email: + label: E-mail + msg: + empty: E-mail skal udfyldes. + password: + label: Adgangskode + msg: + empty: Adgangskoden skal udfyldes. + different: De indtastede adgangskoder er ikke ens + account_forgot: + page_title: Glemt adgangskode + btn_name: Send mig gendannelsesmail + send_success: >- + Hvis en konto matcher {{mail}}, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode. + email: + label: E-mail + msg: + empty: E-mail skal udfyldes. + change_email: + btn_cancel: Annuller + btn_update: Opdater e-mailadresse + send_success: >- + Hvis en konto matcher {{mail}}, vil du modtage en e-mail med instruktioner om, hvordan du nulstiller din adgangskode. + email: + label: Ny e-mail + msg: + empty: E-mail skal udfyldes. + oauth: + connect: Forbind med {{ auth_name }} + remove: Fjern {{ auth_name }} + oauth_bind_email: + subtitle: Tilføj en gendannelsese-mail til din konto. + btn_update: Opdater e-mailadresse + email: + label: E-mail + msg: + empty: E-mail skal udfyldes. + modal_title: Email eksisterer allerede. + modal_content: Denne e-mailadresse er allerede registreret. Er du sikker på, at du vil oprette forbindelse til den eksisterende konto? + modal_cancel: Ændre e-mail + modal_confirm: Opret forbindelse til den eksisterende konto + password_reset: + page_title: Nulstil adgangskode + btn_name: Nulstil min adgangskode + reset_success: >- + Du har ændret din adgangskode. Du vil blive omdirigeret til siden log ind. + link_invalid: >- + Beklager, dette link til nulstilling af adgangskode er ikke længere gyldigt. Måske er din adgangskode allerede nulstillet? + to_login: Fortsæt til log-ind siden + password: + label: Adgangskode + msg: + empty: Adgangskoden skal udfyldes. + length: Længden skal være mellem 8 og 32 tegn + different: De indtastede adgangskoder er ikke ens + password_confirm: + label: Bekræft den nye adgangskode + settings: + page_title: Indstillinger + goto_modify: Gå til at ændre + nav: + profile: Profil + notification: Notifikationer + account: Konto + interface: Grænseflade + profile: + heading: Profil + btn_name: Gem + display_name: + label: Visnings-navn + msg: Visnings-navn skal udfyldes. + msg_range: Display name must be 2-30 characters in length. + username: + label: Brugernavn + caption: Man kan nævne dig som "@username". + msg: Brugernavn skal udfyldes. + msg_range: Username must be 2-30 characters in length. + character: 'Skal bruge tegnsættet "a-z", "0-9", " - . _"' + avatar: + label: Profilbillede + gravatar: Gravatar + gravatar_text: Du kan ændre billede på + custom: Brugerdefineret + custom_text: Du kan uploade dit billede. + default: System + msg: Upload en avatar + bio: + label: Om mig + website: + label: Websted + placeholder: "https://example.com" + msg: Forkert format på websted + location: + label: Placering + placeholder: "By, land" + notification: + heading: Email-notifikationer + turn_on: Slå til + inbox: + label: Notifikationer i indbakken + description: Svar på dine spørgsmål, kommentarer, invitationer og mere. + all_new_question: + label: Alle nye spørgsmål + description: Få besked om alle nye spørgsmål. Op til 50 spørgsmål om ugen. + all_new_question_for_following_tags: + label: Alle nye spørgsmål til følgende tags + description: Få besked om nye spørgsmål til følgende tags. + account: + heading: Konto + change_email_btn: Ændre e-mail + change_pass_btn: Skift adgangskode + change_email_info: >- + Vi har sendt en e-mail til denne adresse. Følg venligst bekræftelsesinstruktionerne. + email: + label: E-mail + new_email: + label: Ny e-mail + msg: Ny e-mail skal udfyldes. + pass: + label: Nuværende adgangskode + msg: Adgangskoden skal udfyldes. + password_title: Adgangskode + current_pass: + label: Nuværende adgangskode + msg: + empty: Nuværende adgangskode skal udfyldes. + length: Længden skal være mellem 8 og 32 tegn. + different: De to indtastede adgangskoder er ikke ens. + new_pass: + label: Ny adgangskode + pass_confirm: + label: Bekræft den nye adgangskode + interface: + heading: Grænseflade + lang: + label: Grænseflade sprog + text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden. + my_logins: + title: Mine log ind + label: Log ind eller tilmeld dig på dette websted ved hjælp af disse konti. + modal_title: Fjern login + modal_content: Er du sikker på, at du vil fjerne dette login fra din konto? + modal_confirm_btn: Slet + remove_success: Fjernet + toast: + update: opdatering gennemført + update_password: Adgangskoden er ændret. + flag_success: Tak for at anmelde. + forbidden_operate_self: Forbudt at operere på dig selv + review: Din revision vil blive vist efter gennemgang. + sent_success: Sendt + related_question: + title: Related + answers: svar + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Inviter personer + desc: Invitér personer, som du tror, kan svare. + invite: Inviter til at svare + add: Tilføj personer + search: Søg personer + question_detail: + action: Handling + Asked: Spurgt + asked: spurgt + update: Ændret + edit: redigeret + commented: kommenteret + Views: Set + Follow: Følg + Following: Følger + follow_tip: Følg dette spørgsmål for at modtage notifikationer + answered: besvaret + closed_in: Lukket om + show_exist: Vis eksisterende spørgsmål. + useful: Nyttigt + question_useful: Det er nyttigt og klart + question_un_useful: Det er uklart eller ikke nyttigt + question_bookmark: Bogmærk dette spørgsmål + answer_useful: Det er nyttigt + answer_un_useful: Det er ikke nyttigt + answers: + title: Svar + score: Bedømmelse + newest: Nyeste + oldest: Ældste + btn_accept: Acceptér + btn_accepted: Accepteret + write_answer: + title: Dit Svar + edit_answer: Redigér mit eksisterende svar + btn_name: Indsend dit svar + add_another_answer: Tilføj endnu et svar + confirm_title: Fortsæt med at svare + continue: Forsæt + confirm_info: >- +

Er du sikker på, at du vil tilføje et andet svar?

Du kan i stedet bruge redigeringslinket til at forfine og forbedre dit eksisterende svar.

+ empty: Svar skal udfyldes. + characters: indhold skal være mindst 6 tegn. + tips: + header_1: Tak for dit svar + li1_1: Vær sikker på at besvare spørgsmålet. Giv oplysninger og del din forskning. + li1_2: Begrund eventuelle udsagn med referencer eller personlige erfaringer. + header_2: Men undgå... + li2_1: Spørger om hjælp, søger afklaring, eller reagerer på andre svar. + reopen: + confirm_btn: Genåbn + title: Genåbn dette indlæg + content: Er du sikker på, at du vil genåbne? + list: + confirm_btn: Liste + title: Sæt dette indlæg på listen + content: Er du sikker på du vil sætte på listen? + unlist: + confirm_btn: Fjern fra listen + title: Fjern dette indlæg fra listen + content: Er du sikker på at du vil fjerne fra listen? + pin: + title: Fastgør dette indlæg + content: Er du sikker på, at du ønsker at fastgøre globalt? Dette indlæg vises øverst på alle indlægs-lister. + confirm_btn: Fastgør + delete: + title: Slet dette indlæg + question: >- + Vi anbefaler ikke, at sletter spørgsmål med svar, fordi det fratager fremtidige læsere denne viden.

Gentaget sletning af besvarede spørgsmål kan resultere i, at din konto bliver blokeret fra at spørge. Er du sikker på, at du ønsker at slette? + answer_accepted: >- +

Vi anbefaler ikke at slette accepteret svar fordi det fratager fremtidige læsere denne viden.

Gentagen sletning af accepterede svar kan resultere i, at din konto bliver blokeret fra besvarelse. Er du sikker på, at du ønsker at slette? + other: Er du sikker på, at du vil slette? + tip_answer_deleted: Dette svar er blevet slettet + undelete_title: Genopret dette indlæg + undelete_desc: Er du sikker på du ønsker at genoprette? + btns: + confirm: Bekræft + cancel: Annuller + edit: Rediger + save: Gem + delete: Slet + undelete: Genopret + list: Sæt på liste + unlist: Fjern fra liste + unlisted: Fjernet fra liste + login: Log ind + signup: Opret konto + logout: Log Ud + verify: Verificér + create: Create + approve: Godkend + reject: Afvis + skip: Spring Over + discard_draft: Kassér udkast + pinned: Fastgjort + all: Alle + question: Spørgsmål + answer: Svar + comment: Kommentar + refresh: Genopfrisk + resend: Send igen + deactivate: Deaktiver + active: Aktiv + suspend: Suspendér + unsuspend: Ophæv suspendering + close: Luk + reopen: Genåbn + ok: Ok + light: Lys + dark: Mørk + system_setting: Systemindstilling + default: Standard + reset: Nulstil + tag: Tag + post_lowercase: indlæg + filter: Filtrer + ignore: Ignorér + submit: Indsend + normal: Normal + closed: Lukket + deleted: Slettet + deleted_permanently: Deleted permanently + pending: Ventende + more: Mere + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Søgeresultater + keywords: Nøgleord + options: Muligheder + follow: Følg + following: Følger + counts: "{{count}} Resultater" + counts_loading: "... Results" + more: Mere + sort_btns: + relevance: Relevans + newest: Nyeste + active: Aktiv + score: Bedømmelse + more: Mere + tips: + title: Avancerede Søgetips + tag: "<1>[tag] søgning med et tag" + user: "<1>user:username søgning efter forfatter" + answer: "<1>answers:0 ubesvarede spørgsmål" + score: "<1>score:3 indlæg med 3+ score" + question: "<1>is:question søgespørgsmål" + is_answer: "<1>is:answer søgesvar" + empty: Vi kunne ikke finde noget.
Prøv forskellige eller mindre specifikke søgeord. + share: + name: Del + copy: Kopiér link + via: Del indlæg via... + copied: Kopieret + facebook: Del på Facebook + twitter: Share to X + cannot_vote_for_self: Du kan ikke stemme på dit eget indlæg. + modal_confirm: + title: Fejl... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Din nye konto er bekræftet. Du vil blive omdirigeret til hjemmesiden. + link: Fortsæt til startside + oops: Hovsa! + invalid: Linket, du brugte, virker ikke længere. + confirm_new_email: Din e-mail er blevet opdateret. + confirm_new_email_invalid: >- + Beklager, dette bekræftelseslink er ikke længere gyldigt. Måske blev din e-mail allerede ændret? + unsubscribe: + page_title: Afmeld + success_title: Afmelding Lykkedes + success_desc: Du er blevet fjernet fra denne abonnentliste og vil ikke modtage yderligere e-mails fra os. + link: Skift indstillinger + question: + following_tags: Følger Tags + edit: Rediger + save: Gem + follow_tag_tip: Følg tags for at udvælge dine spørgsmål. + hot_questions: Populære Spørgsmål + all_questions: Alle Spørgsmål + x_questions: "{{ count }} Spørgsmål" + x_answers: "{{ count }} svar" + x_posts: "{{ count }} Posts" + questions: Spørgsmål + answers: Svar + newest: Nyeste + active: Aktiv + hot: Populært + frequent: Frequent + recommend: Recommend + score: Bedømmelse + unanswered: Ubesvaret + modified: ændret + answered: besvaret + asked: spurgt + closed: lukket + follow_a_tag: Følg et tag + more: Mere + personal: + overview: Oversigt + answers: Svar + answer: svar + questions: Spørgsmål + question: spørgsmål + bookmarks: Bogmærker + reputation: Omdømme + comments: Kommentarer + votes: Stemmer + badges: Badges + newest: Nyeste + score: Bedømmelse + edit_profile: Rediger profil + visited_x_days: "Besøgte {{ count }} dage" + viewed: Set + joined: Tilmeldt + comma: "," + last_login: Set + about_me: Om Mig + about_me_empty: "// Hej, Verden!" + top_answers: Populære Svar + top_questions: Populære Spørgsmål + stats: Statistik + list_empty: Ingen indlæg fundet.
Måske vil du vælge en anden fane? + content_empty: No posts found. + accepted: Accepteret + answered: besvaret + asked: spurgt + downvoted: nedstemt + mod_short: MOD + mod_long: Moderatorer + x_reputation: omdømme + x_votes: stemmer modtaget + x_answers: svar + x_questions: spørgsmål + recent_badges: Recent Badges + install: + title: Installation + next: Næste + done: Udført + config_yaml_error: Kan ikke oprette filen config.yaml. + lang: + label: Vælg et sprog + db_type: + label: Database type + db_username: + label: Brugernavn + placeholder: rod + msg: Brugernavn skal udfyldes. + db_password: + label: Adgangskode + placeholder: rod + msg: Adgangskoden skal udfyldes. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host skal udfyldes. + db_name: + label: Database navn + placeholder: answer + msg: Databasenavn skal udfyldes. + db_file: + label: Databasefil + placeholder: /data/answer.db + msg: Databasefil skal udfyldes. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Opret config.yaml + label: Filen config.yaml blev oprettet. + desc: >- + Du kan manuelt oprette filen <1>config.yaml i mappen <1>/var/wwww/xxx/ og indsætte følgende tekst i den. + info: Når du har gjort det, skal du klikke på "Næste" knappen. + site_information: Websted Information + admin_account: Administrator Konto + site_name: + label: Websted navn + msg: Websted-navn skal udfyldes. + msg_max_length: Webstedsnavn kan ikke være længere end 30 tegn. + site_url: + label: Websted URL + text: Adressen på dit websted. + msg: + empty: Webstedets URL skal udfyldes. + incorrect: Websteds URL forkert format. + max_length: WebstedsURL skal højst være 512 tegn. + contact_email: + label: Kontakt e-mail + text: E-mailadresse på nøglekontakt ansvarlig for dette websted. + msg: + empty: Kontakt-e-mail skal udfyldes. + incorrect: Ugyldig kontakt e-mail adresse. + login_required: + label: Privat + switch: Log ind påkrævet + text: Kun brugere som er logget ind har adgang til dette fællesskab. + admin_name: + label: Navn + msg: Navn skal udfyldes. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Adgangskode + text: >- + Du skal bruge denne adgangskode for at logge ind. Opbevar den et sikkert sted. + msg: Adgangskoden skal udfyldes. + msg_min_length: Adgangskoden skal være mindst 8 tegn. + msg_max_length: Adgangskoden skal højst udgøre 32 tegn. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: E-mail + text: Du skal bruge denne e-mail for at logge ind. + msg: + empty: E-mail skal udfyldes. + incorrect: Ugyldig e-mail adresse. + ready_title: Dit websted er klar + ready_desc: >- + Hvis du nogensinde har lyst til at ændre flere indstillinger, kan du besøge <1>admin-sektion; find det i site-menuen. + good_luck: "Hav det sjovt, og held og lykke!" + warn_title: Advarsel + warn_desc: >- + Filen <1>config.yaml findes allerede. Hvis du har brug for at nulstille en af konfigurationselementerne i denne fil, så slet den først. + install_now: Du kan prøve <1>at installere nu. + installed: Allerede installeret + installed_desc: >- + Du synes allerede at være installeret. For at geninstallere skal du først rydde dine gamle databasetabeller. + db_failed: Database forbindelse mislykkedes + db_failed_desc: >- + Det betyder enten, at databaseinformationen i din <1>config. aml fil er forkert eller at kontakt med databaseserveren ikke kunne etableres. Dette kan betyde, at din værts databaseserver er nede. + counts: + views: visninger + votes: stemmer + answers: svar + accepted: Accepteret + page_error: + http_error: HTTP Fejl {{ code }} + desc_403: Du har ikke adgang til denne side. + desc_404: Denne side findes desværre ikke. + desc_50X: Der skete en fejl på serveren og den kunne ikke fuldføre din anmodning. + back_home: Tilbage til forsiden + page_maintenance: + desc: "Vi laver vedligeholdelse, men er snart tilbage igen." + nav_menus: + dashboard: Kontrolpanel + contents: Indhold + questions: Spørgsmål + answers: Svar + users: Brugere + badges: Badges + flags: Anmeldelser + settings: Indstillinger + general: Generelt + interface: Brugerflade + smtp: SMTP + branding: Branding + legal: Jura + write: Skriv + tos: Betingelser for brug + privacy: Privatliv + seo: SEO + customize: Tilpas + themes: Temaer + login: Log Ind + privileges: Rettigheder + plugins: Plugins + installed_plugins: Installerede Plugins + apperance: Appearance + website_welcome: Velkommen til {{site_name}} + user_center: + login: Log Ind + qrcode_login_tip: Brug {{ agentName }} til at scanne QR-koden og logge ind. + login_failed_email_tip: Log ind mislykkedes, tillad denne app at få adgang til dine e-mail-oplysninger, før du prøver igen. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Administrator + dashboard: + title: Kontrolpanel + welcome: Velkommen til Administration! + site_statistics: Statistik for webstedet + questions: "Spørgsmål:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Svar:" + comments: "Kommentarer:" + votes: "Stemmer:" + users: "Brugere:" + flags: "Anmeldelser:" + reviews: "Gennemgange:" + site_health: Websteds sundhed + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload mappe:" + run_mode: "Kørselstilstand:" + private: Privat + public: Offentlig + smtp: "SMTP:" + timezone: "Tidszone:" + system_info: System information + go_version: "Go version:" + database: "Database:" + database_size: "Database størrelse:" + storage_used: "Anvendt lagerplads:" + uptime: "Oppetid:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Kontakt os + forum: Forum + documents: Dokumenter + feedback: Tilbagemelding + support: Support + review: Gennemgå + config: Konfiguration + update_to: Opdatér til + latest: Seneste + check_failed: Tjek mislykkedes + "yes": "Ja" + "no": "Nej" + not_allowed: Ikke tilladt + allowed: Tilladt + enabled: Aktiveret + disabled: Deaktiveret + writable: Skrivbar + not_writable: Ikke skrivbar + flags: + title: Anmeldelser + pending: Ventende + completed: Gennemført + flagged: Anmeldt + flagged_type: Anmeldt{{ type }} + created: Oprettet + action: Handling + review: Gennemgå + user_role_modal: + title: Skift brugerrolle til... + btn_cancel: Annuller + btn_submit: Indsend + new_password_modal: + title: Angiv ny adgangskode + form: + fields: + password: + label: Adgangskode + text: Brugeren vil blive logget ud og skal logge ind igen. + msg: Adgangskoden skal være på 8- 32 tegn. + btn_cancel: Annuller + btn_submit: Indsend + edit_profile_modal: + title: Rediger profil + form: + fields: + display_name: + label: Visnings-navn + msg_range: Display name must be 2-30 characters in length. + username: + label: Brugernavn + msg_range: Username must be 2-30 characters in length. + email: + label: E-mail + msg_invalid: Ugyldig E-Mail Adresse. + edit_success: Redigering lykkedes + btn_cancel: Annuller + btn_submit: Indsend + user_modal: + title: Tilføj ny bruger + form: + fields: + users: + label: Masse-tilføj bruger + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Adskil “navn, e-mail, adgangskode” med kommaer. Én bruger pr. linje. + msg: "Indtast venligst brugerens e-mail, en pr. linje." + display_name: + label: Visnings-navn + msg: Display name must be 2-30 characters in length. + email: + label: E-mail + msg: E-mail er ugyldig. + password: + label: Adgangskode + msg: Adgangskoden skal være 8- 32 tegn. + btn_cancel: Annuller + btn_submit: Indsend + users: + title: Brugere + name: Navn + email: E-mail + reputation: Omdømme + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Rolle + action: Handling + change: Ændre + all: Alle + staff: Ansatte + more: Mere + inactive: Inaktiv + suspended: Suspenderet + deleted: Slettet + normal: Normal + Moderator: Moderator + Admin: Administrator + User: Bruger + filter: + placeholder: "Filtrer efter navn, user:id" + set_new_password: Angiv ny adgangskode + edit_profile: Rediger profil + change_status: Ændre status + change_role: Ændre rolle + show_logs: Vis logfiler + add_user: Tilføj bruger + deactivate_user: + title: Deaktiver bruger + content: En inaktiv bruger skal bekræfte deres e-mail igen. + delete_user: + title: Slet denne bruger + content: Er du sikker på, at du vil slette denne bruger? Dette er permanent! + remove: Fjern deres indhold + label: Fjern alle spørgsmål, svar, kommentarer osv. + text: Tjek ikke dette, hvis du kun ønsker at slette brugerens konto. + suspend_user: + title: Suspendér denne bruger + content: En suspenderet bruger kan ikke logge ind. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Spørgsmål + unlisted: Fjernet fra liste + post: Indlæg + votes: Stemmer + answers: Svar + created: Oprettet + status: Status + action: Handling + change: Ændre + pending: Ventende + filter: + placeholder: "Filtrer efter titel, question:id" + answers: + page_title: Svar + post: Indlæg + votes: Stemmer + created: Oprettet + status: Status + action: Handling + change: Ændre + filter: + placeholder: "Filtrer efter titel, answer:id" + general: + page_title: Generelt + name: + label: Websted navn + msg: Websted-navn skal udfyldes. + text: "Navnet på dette websted, som bruges i title-tagget." + site_url: + label: Websted URL + msg: Websted-URL skal udfyldes. + validate: Angiv et gyldigt URL. + text: Adressen på dit websted. + short_desc: + label: Kort beskrivelse af websted + msg: Kort beskrivelse af websted skal udfyldes. + text: "Kort beskrivelse, som anvendt i title-tag på hjemmesiden." + desc: + label: Websted beskrivelse + msg: Webstedsbeskrivelse skal udfyldes. + text: "Beskriv dette websted i en sætning, som bruges i meta description tagget." + contact_email: + label: Kontakt e-mail + msg: Kontakt-e-mail skal udfyldes. + validate: Kontakt-e-mail er ugyldig. + text: E-mailadresse på nøglekontakt ansvarlig for dette websted. + check_update: + label: Opdatering af software + text: Søg automatisk efter opdateringer + interface: + page_title: Brugerflade + language: + label: Brugerflade sprog + msg: Brugerflade-sprog skal udfyldes. + text: Brugergrænseflade sprog. Det vil ændres, når du opdaterer siden. + time_zone: + label: Tidszone + msg: Tidszone skal udfyldes. + text: Vælg en by i samme tidszone som dig selv. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Fra e-mail + msg: Fra e-mail skal udfyldes. + text: E-mail-adressen som e-mails sendes fra. + from_name: + label: Fra navn + msg: Fra navn skal udfyldes. + text: Navnet som e-mails sendes fra. + smtp_host: + label: SMTP host + msg: SMTP host skal udfyldes. + text: Din mail-server. + encryption: + label: Kryptering + msg: Kryptering skal udfyldes. + text: For de fleste servere er SSL den anbefalede indstilling. + ssl: SSL + tls: TLS + none: Ingen + smtp_port: + label: SMTP port + msg: SMTP port skal være nummer 1 ~ 65535. + text: Porten til din mailserver. + smtp_username: + label: SMTP brugernavn + msg: SMTP brugernavn skal udfyldes. + smtp_password: + label: SMTP adgangskode + msg: SMTP adgangskode skal udfyldes. + test_email_recipient: + label: Test e-mail modtagere + text: Angiv e-mail-adresse, der vil modtage test-beskedder. + msg: Test e-mail modtagere er ugyldige + smtp_authentication: + label: Aktiver autentificering + title: SMTP autentificering + msg: SMTP autentificering skal udfyldes. + "yes": "Ja" + "no": "Nej" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo skal udfyldes. + text: Logoet billede øverst til venstre på dit websted. Brug et bredt rektangulært billede med en højde på 56 og et breddeforhold større end 3:1. Hvis efterladt tom, vil webstedets titeltekst blive vist. + mobile_logo: + label: Mobil logo + text: Logoet bruges på mobile version af dit websted. Brug et bredt rektangulært billede med en højde på 56. Hvis efterladt tom, vil billedet fra indstillingen "logo" blive brugt. + square_icon: + label: Kvadratisk ikon + msg: Kvadratisk ikon skal udfyldes. + text: Billede brugt som basis for metadata-ikoner. Bør være større end 512x512. + favicon: + label: Favicon + text: En favicon til dit websted. For at fungere korrekt over en CDN skal det være en png. Vil blive ændret til 32x32. Hvis efterladt tomt, vil "firkantet ikon" blive brugt. + legal: + page_title: Jura + terms_of_service: + label: Betingelser for brug + text: "Du kan tilføje servicevilkår her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her." + privacy_policy: + label: Privatlivspolitik + text: "Du kan tilføje privatlivspolitik indhold her. Hvis du allerede har et dokument hostet et andet sted, så angiv den fulde URL her." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Skriv + restrict_answer: + title: Skriv svar + label: Hver bruger kan kun skrive et svar for det samme spørgsmål + text: "Slå fra for at give brugerne mulighed for at skrive flere svar på det samme spørgsmål, hvilket kan forårsage svar at være ufokuseret." + recommend_tags: + label: Anbefal tags + text: "Anbefal tags vil som standard blive vist i dropdown-listen." + msg: + contain_reserved: "anbefalede tags kan ikke indeholde reserverede tags" + required_tag: + title: Angiv påkrævede tags + label: Sæt “Anbefal tags” som påkrævede tags + text: "Hvert nyt spørgsmål skal have mindst et anbefalet tag." + reserved_tags: + label: Reserverede tags + text: "Reserverede tags kan kun bruges af moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Brugerdefinerede URL-strukturer kan forbedre brugervenlighed og fremadrettet kompatibilitet af dine links. + robots: + label: robots.txt + text: Dette vil permanent tilsidesætte eventuelle relaterede webstedsindstillinger. + themes: + page_title: Temaer + themes: + label: Temaer + text: Vælg et eksisterende tema. + color_scheme: + label: Farveskema + navbar_style: + label: Navbar background style + primary_color: + label: Primær farve + text: Ændre farver, der bruges af dine temaer + css_and_html: + page_title: CSS og HTML + custom_css: + label: Brugerdefineret CSS + text: > + + head: + label: Head + text: > + + header: + label: Overskrift + text: > + + footer: + label: Sidefod + text: Dette indsættes før </body>. + sidebar: + label: Sidebjælke + text: Dette vil indsætte i sidebjælken. + login: + page_title: Log Ind + membership: + title: Medlemskab + label: Tillad nye registreringer + text: Slå fra for at forhindre at nogen opretter en ny konto. + email_registration: + title: E-mail-registrering + label: Tillad e-mail registrering + text: Slå fra for at forhindre, at der oprettes en ny konto via e-mail. + allowed_email_domains: + title: Tilladte e-mail-domæner + text: E-mail-domæner som brugere skal registrere konti med. Et domæne pr. linje. Ignoreres når tomt. + private: + title: Privat + label: Log ind påkrævet + text: Kun brugere som er logget ind har adgang til dette fællesskab. + password_login: + title: Adgangskode log ind + label: Tillad e-mail og adgangskode login + text: "ADVARSEL: Hvis du slår fra, kan du muligvis ikke logge ind, hvis du ikke tidligere har konfigureret en anden loginmetode." + installed_plugins: + title: Installerede Plugins + plugin_link: Plugins udvider og udvider funktionaliteten. Du kan finde plugins i <1>Plugin Repository. + filter: + all: Alle + active: Aktiv + inactive: Inaktiv + outdated: Forældet + plugins: + label: Plugins + text: Vælg et eksisterende plugin. + name: Navn + version: Version + status: Status + action: Handling + deactivate: Deaktiver + activate: Aktivér + settings: Indstillinger + settings_users: + title: Brugere + avatar: + label: Standard avatar + text: For brugere uden en brugerdefineret avatar. + gravatar_base_url: + label: Gravatar base-URL + text: URL for Gravatar-udbyderens API-base. Ignoreres når tom. + profile_editable: + title: Profil redigerbar + allow_update_display_name: + label: Tillad brugere at ændre deres visningsnavn + allow_update_username: + label: Tillad brugere at ændre deres brugernavn + allow_update_avatar: + label: Tillad brugere at ændre deres profilbillede + allow_update_bio: + label: Tillad brugere at ændre deres om-mig + allow_update_website: + label: Tillad brugere at ændre deres hjemmeside + allow_update_location: + label: Tillad brugere at ændre deres placering + privilege: + title: Rettigheder + level: + label: Omdømme påkrævet niveau + text: Vælg det omdømme der kræves for rettighederne + msg: + should_be_number: input skal være et tal + number_larger_1: tal skal være lig med eller større end 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (valgfrit) + empty: skal udfyldes + invalid: er ugyldigt + btn_submit: Gem + not_found_props: "Nødvendig egenskab {{ key }} ikke fundet." + select: Vælg + page_review: + review: Gennemgå + proposed: foreslået + question_edit: Rediger spørgsmål + answer_edit: Svar redigér + tag_edit: Tag redigér + edit_summary: Rediger resumé + edit_question: Rediger spørgsmål + edit_answer: Rediger svar + edit_tag: Rediger tag + empty: Ingen gennemgangsopgaver tilbage. + approve_revision_tip: Godkender du denne revision? + approve_flag_tip: Godkender du denne anmeldelse? + approve_post_tip: Godkender du dette indlæg? + approve_user_tip: Godkender du denne bruger? + suggest_edits: Foreslåede redigeringer + flag_post: Anmeld indlæg + flag_user: Anmeld bruger + queued_post: Indlæg i kø + queued_user: Brugere i kø + filter_label: Type + reputation: omdømme + flag_post_type: Anmeld dette indlæg som {{ type }}. + flag_user_type: Anmeldte dette indlæg som {{ type }}. + edit_post: Rediger opslag + list_post: Sæt indlæg på liste + unlist_post: Fjern indlæg fra liste + timeline: + undeleted: genskabt + deleted: slettet + downvote: stem ned + upvote: stem op + accept: acceptér + cancelled: annulleret + commented: kommenteret + rollback: tilbagerul + edited: redigeret + answered: besvaret + asked: spurgt + closed: lukket + reopened: genåbnet + created: oprettet + pin: fastgjort + unpin: frigjort + show: sat på liste + hide: fjernet fra liste + title: "Historik for" + tag_title: "Tidslinje for" + show_votes: "Vis stemmer" + n_or_a: Ikke Relevant + title_for_question: "Tidslinje for" + title_for_answer: "Tidslinje for svar på {{ title }} af {{ author }}" + title_for_tag: "Tidslinje for tag" + datetime: Datetime + type: Type + by: Af + comment: Kommentar + no_data: "Vi kunne ikke finde noget." + users: + title: Brugere + users_with_the_most_reputation: Brugere med det højeste omdømme scorer denne uge + users_with_the_most_vote: Brugere, der stemte mest i denne uge + staffs: Vores fællesskabs personale + reputation: omdømme + votes: stemmer + prompt: + leave_page: Er du sikker på, at du vil forlade siden? + changes_not_save: Dine ændringer er muligvis ikke gemt. + draft: + discard_confirm: Er du sikker på, at du vil kassere dit udkast? + messages: + post_deleted: Dette indlæg er blevet slettet. + post_cancel_deleted: This post has been undeleted. + post_pin: Dette indlæg er blevet fastgjort. + post_unpin: Dette indlæg er blevet frigjort. + post_hide_list: Dette indlæg er blevet skjult fra listen. + post_show_list: Dette indlæg er blevet vist på listen. + post_reopen: Dette indlæg er blevet genåbnet. + post_list: Dette indlæg er blevet listet. + post_unlist: Dette indlæg er blevet aflistet. + post_pending: Dit indlæg afventer gennemgang. Dette er en forhåndsvisning, det vil være synligt, når det er blevet godkendt. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/de_DE.yaml b/i18n/de_DE.yaml new file mode 100644 index 000000000..b68e874de --- /dev/null +++ b/i18n/de_DE.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Erfolgreich. + unknown: + other: Unbekannter Fehler. + request_format_error: + other: Format der Anfrage ist ungültig. + unauthorized_error: + other: Nicht autorisiert. + database_error: + other: Datenbank-Fehler. + forbidden_error: + other: Verboten. + duplicate_request_error: + other: Doppelte Einreichung. + action: + report: + other: Melden + edit: + other: Bearbeiten + delete: + other: Löschen + close: + other: Schließen + reopen: + other: Wieder öffnen + forbidden_error: + other: Verboten. + pin: + other: Anpinnen + hide: + other: Von Liste nehmen + unpin: + other: Loslösen + show: + other: Liste + invite_someone_to_answer: + other: Bearbeiten + undelete: + other: Wiederherstellen + merge: + other: Zusammenführen + role: + name: + user: + other: Benutzer + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Standard ohne speziellen Zugriff. + admin: + other: Habe die volle Berechtigung, auf die Seite zuzugreifen. + moderator: + other: Hat Zugriff auf alle Beiträge außer Admin-Einstellungen. + privilege: + level_1: + description: + other: Level 1 (weniger Reputation für privates Team, Gruppen) + level_2: + description: + other: Level 2 (niedrige Reputation für Startup-Community) + level_3: + description: + other: Level 3 (hohe Reputation für eine reife Community) + level_custom: + description: + other: Benutzerdefinierter Level + rank_question_add_label: + other: Fragen stellen + rank_answer_add_label: + other: Antwort schreiben + rank_comment_add_label: + other: Kommentar schreiben + rank_report_add_label: + other: Melden + rank_comment_vote_up_label: + other: Kommentar upvoten + rank_link_url_limit_label: + other: Mehr als 2 Links gleichzeitig posten + rank_question_vote_up_label: + other: Frage upvoten + rank_answer_vote_up_label: + other: Antwort upvoten + rank_question_vote_down_label: + other: Frage downvoten + rank_answer_vote_down_label: + other: Antwort downvoten + rank_invite_someone_to_answer_label: + other: Jemanden zum Antworten einladen + rank_tag_add_label: + other: Neuen Tag erstellen + rank_tag_edit_label: + other: Tag-Beschreibung bearbeiten (muss überprüft werden) + rank_question_edit_label: + other: Frage eines anderen bearbeiten (muss überarbeitet werden) + rank_answer_edit_label: + other: Antwort eines anderen bearbeiten (muss überarbeitet werden) + rank_question_edit_without_review_label: + other: Frage eines anderen ohne Überprüfung bearbeiten + rank_answer_edit_without_review_label: + other: Antwort eines anderen ohne Überprüfung bearbeiten + rank_question_audit_label: + other: Frageänderungen überprüfen + rank_answer_audit_label: + other: Bearbeitete Antworten überprüfen + rank_tag_audit_label: + other: Tag-Bearbeitungen überprüfen + rank_tag_edit_without_review_label: + other: Tag-Beschreibung ohne Überprüfung bearbeiten + rank_tag_synonym_label: + other: Tag-Synonyme verwalten + email: + other: E-Mail + e_mail: + other: E-Mail + password: + other: Passwort + pass: + other: Passwort + old_pass: + other: Aktuelles Passwort + original_text: + other: Dieser Beitrag + email_or_password_wrong_error: + other: E-Mail und Passwort stimmen nicht überein. + error: + common: + invalid_url: + other: Ungültige URL. + status_invalid: + other: Ungültiger Status. + password: + space_invalid: + other: Passwort darf keine Leerzeichen enthalten. + admin: + cannot_update_their_password: + other: Du kannst dein Passwort nicht ändern. + cannot_edit_their_profile: + other: Du kannst dein Profil nicht bearbeiten. + cannot_modify_self_status: + other: Du kannst deinen Status nicht ändern. + email_or_password_wrong: + other: E-Mail und Password stimmen nicht überein. + answer: + not_found: + other: Antwort nicht gefunden. + cannot_deleted: + other: Keine Berechtigung zum Löschen. + cannot_update: + other: Keine Berechtigung zum Aktualisieren. + question_closed_cannot_add: + other: Fragen sind geschlossen und können nicht hinzugefügt werden. + content_cannot_empty: + other: Die Antwort darf nicht leer sein. + comment: + edit_without_permission: + other: Kommentar kann nicht bearbeitet werden. + not_found: + other: Kommentar wurde nicht gefunden. + cannot_edit_after_deadline: + other: Die Kommentarzeit war zu lang, um sie zu ändern. + content_cannot_empty: + other: Der Kommentar darf nicht leer sein. + email: + duplicate: + other: E-Mail existiert bereits. + need_to_be_verified: + other: E-Mail muss überprüft werden. + verify_url_expired: + other: Die verifizierbare E-Mail-URL ist abgelaufen, bitte sende die E-Mail erneut. + illegal_email_domain_error: + other: E-Mails sind von dieser E-Mail-Domäne nicht erlaubt. Bitte verwende eine andere. + lang: + not_found: + other: Sprachdatei nicht gefunden. + object: + captcha_verification_failed: + other: Captcha ist falsch. + disallow_follow: + other: Es ist dir nicht erlaubt zu folgen. + disallow_vote: + other: Es ist dir nicht erlaubt abzustimmen. + disallow_vote_your_self: + other: Du kannst nicht für deinen eigenen Beitrag stimmen. + not_found: + other: Objekt nicht gefunden. + verification_failed: + other: Verifizierung fehlgeschlagen. + email_or_password_incorrect: + other: E-Mail und Passwort stimmen nicht überein. + old_password_verification_failed: + other: Die Überprüfung des alten Passworts ist fehlgeschlagen + new_password_same_as_previous_setting: + other: Das neue Passwort ist das gleiche wie das vorherige Passwort. + already_deleted: + other: Dieser Beitrag wurde gelöscht. + meta: + object_not_found: + other: Metaobjekt nicht gefunden + question: + already_deleted: + other: Dieser Beitrag wurde gelöscht. + under_review: + other: Ihr Beitrag wartet auf Überprüfung. Er wird sichtbar sein, nachdem er genehmigt wurde. + not_found: + other: Frage nicht gefunden. + cannot_deleted: + other: Keine Berechtigung zum Löschen. + cannot_close: + other: Keine Berechtigung zum Schließen. + cannot_update: + other: Keine Berechtigung zum Aktualisieren. + content_cannot_empty: + other: Der Inhalt darf nicht leer sein. + rank: + fail_to_meet_the_condition: + other: Ansehenssrang erfüllt die Bedingung nicht. + vote_fail_to_meet_the_condition: + other: Danke für dein Feedback. Du brauchst mindestens {{.Rank}} Ansehen, um eine Stimme abzugeben. + no_enough_rank_to_operate: + other: Dafür brauchst du mindestens {{.Rank}} Ansehen. + report: + handle_failed: + other: Bearbeiten der Meldung fehlgeschlagen. + not_found: + other: Meldung nicht gefunden. + tag: + already_exist: + other: Tag existiert bereits. + not_found: + other: Tag nicht gefunden. + recommend_tag_not_found: + other: Das Tag "Empfehlen" ist nicht vorhanden. + recommend_tag_enter: + other: Bitte gib mindestens einen erforderlichen Tag ein. + not_contain_synonym_tags: + other: Sollte keine Synonym-Tags enthalten. + cannot_update: + other: Keine Berechtigung zum Aktualisieren. + is_used_cannot_delete: + other: Du kannst keinen Tag löschen, der in Gebrauch ist. + cannot_set_synonym_as_itself: + other: Du kannst das Synonym des aktuellen Tags nicht als sich selbst festlegen. + smtp: + config_from_name_cannot_be_email: + other: Der Absendername kann keine E-Mail-Adresse sein. + theme: + not_found: + other: Design nicht gefunden. + revision: + review_underway: + other: Kann derzeit nicht bearbeitet werden, es existiert eine Version in der Überprüfungswarteschlange. + no_permission: + other: Keine Berechtigung zum Überarbeiten. + user: + external_login_missing_user_id: + other: Die Plattform des Drittanbieters stellt keine eindeutige UserID zur Verfügung, sodass du dich nicht anmelden kannst. Bitte wende dich an den Administrator der Website. + external_login_unbinding_forbidden: + other: Bitte setze ein Login-Passwort für dein Konto, bevor du dieses Login entfernst. + email_or_password_wrong: + other: + other: E-Mail und Passwort stimmen nicht überein. + not_found: + other: Benutzer nicht gefunden. + suspended: + other: Benutzer wurde gesperrt. + username_invalid: + other: Benutzername ist ungültig. + username_duplicate: + other: Benutzername wird bereits verwendet. + set_avatar: + other: Avatar setzen fehlgeschlagen. + cannot_update_your_role: + other: Du kannst deine Rolle nicht ändern. + not_allowed_registration: + other: Derzeit ist die Seite nicht für die Anmeldung geöffnet. + not_allowed_login_via_password: + other: Zurzeit ist es auf der Seite nicht möglich, sich mit einem Passwort anzumelden. + access_denied: + other: Zugriff verweigert + page_access_denied: + other: Du hast keinen Zugriff auf diese Seite. + add_bulk_users_format_error: + other: "Fehler {{.Field}}-Format in der Nähe von '{{.Content}}' in Zeile {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Die Anzahl der Benutzer, die du auf einmal hinzufügst, sollte im Bereich von 1-{{.MaxAmount}} liegen." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Lesekonfiguration fehlgeschlagen + database: + connection_failed: + other: Datenbankverbindung fehlgeschlagen + create_table_failed: + other: Tabelle erstellen fehlgeschlagen + install: + create_config_failed: + other: Kann die config.yaml-Datei nicht erstellen. + upload: + unsupported_file_format: + other: Dateiformat nicht unterstützt. + site_info: + config_not_found: + other: Seiten-Konfiguration nicht gefunden. + badge: + object_not_found: + other: Abzeichen-Objekt nicht gefunden + reason: + spam: + name: + other: Spam + desc: + other: Dieser Beitrag ist eine Werbung oder Vandalismus. Er ist nicht nützlich oder relevant für das aktuelle Thema. + rude_or_abusive: + name: + other: unhöflich oder beleidigend + desc: + other: "Eine vernünftige Person würde diesen Inhalt im respektvoll diskutierten Diskurs für unangemessen halten." + a_duplicate: + name: + other: ein Duplikat + desc: + other: Diese Frage wurde schon einmal gestellt und hat bereits eine Antwort. + placeholder: + other: Gib den Link zur bestehenden Frage ein + not_a_answer: + name: + other: keine Antwort + desc: + other: "Die Antwort versucht nicht, die Frage zu beantworten. Sie sollte entweder bearbeitet, kommentiert, als weitere Frage gestellt oder ganz gelöscht werden." + no_longer_needed: + name: + other: nicht mehr benötigt + desc: + other: Dieser Kommentar ist veraltet oder nicht relevant für diesen Beitrag. + something: + name: + other: anderer Grund + desc: + other: Dieser Beitrag erfordert die Aufmerksamkeit der Temmitglieder aus einem anderen, oben nicht genannten Grund. + placeholder: + other: Lass uns wissen, worüber du dir Sorgen machst + community_specific: + name: + other: ein Community-spezifischer Grund + desc: + other: Diese Frage entspricht nicht den Gemeinschaftsrichtlinien. + not_clarity: + name: + other: benötigt Details oder Klarheit + desc: + other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren. + looks_ok: + name: + other: sieht OK aus + desc: + other: Dieser Beitrag ist gut so wie er ist und nicht von schlechter Qualität. + needs_edit: + name: + other: muss bearbeitet werden, und ich habe es getan + desc: + other: Verbessere und korrigiere Probleme mit diesem Beitrag selbst. + needs_close: + name: + other: muss geschlossen werden + desc: + other: Eine geschlossene Frage kann nicht beantwortet werden, aber du kannst sie trotzdem bearbeiten, abstimmen und kommentieren. + needs_delete: + name: + other: muss gelöscht werden + desc: + other: Dieser Beitrag wird gelöscht. + question: + close: + duplicate: + name: + other: Spam + desc: + other: Diese Frage ist bereits gestellt worden und hat bereits eine Antwort. + guideline: + name: + other: ein Community-spezifischer Grund + desc: + other: Diese Frage entspricht nicht einer Gemeinschaftsrichtlinie. + multiple: + name: + other: benötigt Details oder Klarheit + desc: + other: Diese Frage enthält derzeit mehrere Fragen in einer. Sie sollte sich auf ein einziges Problem konzentrieren. + other: + name: + other: etwas anderes + desc: + other: Dieser Beitrag erfordert einen anderen Grund, der oben nicht aufgeführt ist. + operation_type: + asked: + other: gefragt + answered: + other: beantwortet + modified: + other: geändert + deleted_title: + other: Gelöschte Frage + questions_title: + other: Fragen + tag: + tags_title: + other: Schlagwörter + no_description: + other: Diese Kategorie hat keine Beschreibung. + notification: + action: + update_question: + other: aktualisierte Frage + answer_the_question: + other: beantwortete Frage + update_answer: + other: aktualisierte Antwort + accept_answer: + other: akzeptierte Antwort + comment_question: + other: kommentierte Frage + comment_answer: + other: kommentierte Antwort + reply_to_you: + other: hat Ihnen geantwortet + mention_you: + other: hat dich erwähnt + your_question_is_closed: + other: Deine Frage wurde geschlossen + your_question_was_deleted: + other: Deine Frage wurde gelöscht + your_answer_was_deleted: + other: Deine Antwort wurde gelöscht + your_comment_was_deleted: + other: Dein Kommentar wurde gelöscht + up_voted_question: + other: positiv bewertete Frage + down_voted_question: + other: negativ bewertete Frage + up_voted_answer: + other: positiv bewertete Antwort + down_voted_answer: + other: negativ bewertete Antwort + up_voted_comment: + other: positiv bewerteter Kommentar + invited_you_to_answer: + other: hat dich eingeladen, zu antworten + earned_badge: + other: Du hast das "{{.BadgeName}}" Abzeichen verdient + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Bestätige deine neue E-Mail-Adresse" + body: + other: "Bestätigen Sie Ihre neue E-Mail-Adresse für {{.SiteName}} indem Sie auf den folgenden Link klicken:
\n{{.ChangeEmailUrl}}

\n\nWenn Sie diese Änderung nicht angefordert haben bitte diese E-Mail ignorieren.

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} hat deine Frage beantwortet" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n\nAuf {{.SiteName}} anschauen

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} hat dich eingeladen zu antworten" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Ich denke, Sie kennen die Antwort.

\n {{.SiteName}}

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} hat deinen Beitrag kommentiert" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n\nAuf {{.SiteName}} anschauen

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird.

\n\nAbmelden" + new_question: + title: + other: "[{{.SiteName}}] Neue Frage: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nHinweis: Dies ist eine automatische Systemnachricht, bitte antworten Sie nicht darauf. Antworten werden nicht gelesen oder bearbeitet.

\n\nBenachrichtigung abbestellen" + pass_reset: + title: + other: "[{{.SiteName }}] Passwort zurücksetzen" + body: + other: "Jemand bat darum, Ihr Passwort auf {{.SiteName}}zurückzusetzen.

\n\nWenn Sie es nicht waren, können Sie diese E-Mail sicher ignorieren.

\n\nKlicken Sie auf den folgenden Link, um ein neues Passwort auszuwählen:
\n{{.PassResetUrl}}\n\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." + register: + title: + other: "[{{.SiteName}}] Bestätige dein neues Konto" + body: + other: "Willkommen in {{.SiteName}}!

\n\nKlicken Sie auf den folgenden Link, um Ihr neues Konto zu bestätigen und zu aktivieren:
\n{{.RegisterUrl}}

\n\nWenn der obige Link nicht anklickbar ist kopieren und in die Adressleiste Ihres Webbrowsers einfügen.\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht sichtbar ist." + test: + title: + other: "[{{.SiteName}}] Test-E-Mail" + body: + other: "Dies ist eine Test-E-Mail.\n

\n\n--
\nHinweis: Dies ist eine automatische System-E-Mail, Bitte antworten Sie nicht auf diese Nachricht, da Ihre Antwort nicht angezeigt wird." + action_activity_type: + upvote: + other: positiv bewerten + upvoted: + other: positiv bewertet + downvote: + other: negativ bewerten + downvoted: + other: negativ bewertet + accept: + other: akzeptieren + accepted: + other: akzeptiert + edit: + other: bearbeiten + review: + queued_post: + other: Post in der Warteschlange + flagged_post: + other: Beiträge gemeldet + suggested_post_edit: + other: Änderungsvorschläge + reaction: + tooltip: + other: "{{ .Names }} Und {{ .Count }} mehr..." + badge: + default_badges: + autobiographer: + name: + other: Autobiograph + desc: + other: Gefüllt mit Profil Informationen. + certified: + name: + other: Zertifiziert + desc: + other: Erledigte unser neues Benutzerhandbuch. + editor: + name: + other: Editor + desc: + other: Erster Beitrag bearbeiten. + first_flag: + name: + other: Erste Meldung + desc: + other: Erste Meldung eines Beitrags. + first_upvote: + name: + other: Erster Upvote + desc: + other: Erste Like eines Beitrags. + first_link: + name: + other: Erster Link + desc: + other: Hat erstmals einen Link zu einem anderen Beitrag hinzugefügt. + first_reaction: + name: + other: Erste Reaktion + desc: + other: Zuerst reagierte auf den Beitrag. + first_share: + name: + other: Erstes Teilen + desc: + other: Zuerst einen Beitrag geteilt. + scholar: + name: + other: Gelehrter + desc: + other: Hat eine Frage gestellt und eine Antwort akzeptiert. + commentator: + name: + other: Kommentator + desc: + other: Hinterlassen Sie 5 Kommentare. + new_user_of_the_month: + name: + other: Neuer Benutzer des Monats + desc: + other: Ausstehende Beiträge in ihrem ersten Monat. + read_guidelines: + name: + other: Lesen Sie die Richtlinien + desc: + other: Lesen Sie die [Community-Richtlinien]. + reader: + name: + other: Leser + desc: + other: Lesen Sie alle Antworten in einem Thema mit mehr als 10 Antworten. + welcome: + name: + other: Willkommen + desc: + other: Du hast eine positive Abstimmung erhalten. + nice_share: + name: + other: Schöne teilen + desc: + other: Hat einen Beitrag mit 25 einzigartigen Besuchern freigegeben. + good_share: + name: + other: Gut geteilt + desc: + other: Hat einen Beitrag mit 300 einzigartigen Besuchern freigegeben. + great_share: + name: + other: Großartiges Teilen + desc: + other: Hat einen Beitrag mit 1000 einzigartigen Besuchern freigegeben. + out_of_love: + name: + other: Aus Liebe + desc: + other: Hat an einem Tag 50 Upvotes verwendet. + higher_love: + name: + other: Höhere Liebe + desc: + other: Hat an einem Tag 50 Upvotes 5 Mal verwendet. + crazy_in_love: + name: + other: Im siebten Himmel + desc: + other: Hat an einem Tag 50 Upvotes 20 Mal verwendet. + promoter: + name: + other: Förderer + desc: + other: Hat einen Benutzer eingeladen. + campaigner: + name: + other: Kampagnenleiter + desc: + other: Lade 3 einfache Benutzer ein. + champion: + name: + other: Champion + desc: + other: Hat 5 Mitglieder eingeladen. + thank_you: + name: + other: Vielen Dank + desc: + other: Beitrag mit 20 Upvotes und 10 abgegebenen Upvotes. + gives_back: + name: + other: Feedback geben + desc: + other: Beitrag mit 100 Upvotes und 100 abgegebenen Upvotes. + empathetic: + name: + other: Einfühlsam + desc: + other: Beitrag mit 500 Upvotes und 1000 abgegebenen Upvotes. + enthusiast: + name: + other: Enthusiast + desc: + other: Besucht 10 aufeinander folgende Tage. + aficionado: + name: + other: Aficionado + desc: + other: Besucht 100 aufeinander folgende Tage. + devotee: + name: + other: Anhänger + desc: + other: 365 aufeinander folgende Tage besucht. + anniversary: + name: + other: Jahrestag + desc: + other: Aktives Mitglied für ein Jahr, mindestens einmal veröffentlicht. + appreciated: + name: + other: Gewertschätzt + desc: + other: Erhalten 1 up vote für 20 posts. + respected: + name: + other: Respektiert + desc: + other: Erhalten 2 up vote für 100 posts. + admired: + name: + other: Bewundert + desc: + other: 5 upvotes für 300 posts erhalten. + solved: + name: + other: Gelöst + desc: + other: Eine Antwort wurde akzeptiert. + guidance_counsellor: + name: + other: Anleitungsberater + desc: + other: 10 Antworten wurden akzeptiert. + know_it_all: + name: + other: Alleswisser + desc: + other: 50 Antworten wurden akzeptiert. + solution_institution: + name: + other: Lösungsfinder + desc: + other: 150 Antworten wurden akzeptiert. + nice_answer: + name: + other: Nette Antwort + desc: + other: Die Antwortpunktzahl beträgt mehr als 10 Punkte. + good_answer: + name: + other: Gute Antwort + desc: + other: Die Antwortpunktzahl beträgt mehr als 25 Punkte. + great_answer: + name: + other: Großartige Antwort + desc: + other: Die Antwortpunktzahl beträgt mehr als 50 Punkte. + nice_question: + name: + other: Schöne Frage + desc: + other: Fragenpunktzahl von 10 oder mehr. + good_question: + name: + other: Gute Frage + desc: + other: Fragen mit 25 oder mehr Punkten. + great_question: + name: + other: Große Frage + desc: + other: Frage mit 50 oder mehr Punkten. + popular_question: + name: + other: Populäre Frage + desc: + other: Frage mit 500 Ansichten. + notable_question: + name: + other: Bemerkenswerte Frage + desc: + other: Frage mit 1.000 Ansichten. + famous_question: + name: + other: Erstklassige Frage + desc: + other: Frage mit 5.000 Ansichten. + popular_link: + name: + other: Populärer Link + desc: + other: Hat einen externen Link mit 50 Klicks gepostet. + hot_link: + name: + other: Heißer Link + desc: + other: Geschrieben einen externen Link mit 300 Klicks. + famous_link: + name: + other: Berühmter Link + desc: + other: Geschrieben einen externen Link mit 100 Klicks. + default_badge_groups: + getting_started: + name: + other: Erste Schritte + community: + name: + other: Gemeinschaft + posting: + name: + other: Freigeben +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Wie man formatiert + desc: >- + + pagination: + prev: Zurück + next: Weiter + page_title: + question: Frage + questions: Fragen + tag: Schlagwort + tags: Schlagwörter + tag_wiki: tag Wiki + create_tag: Tag erstellen + edit_tag: Tag bearbeiten + ask_a_question: Create Question + edit_question: Frage bearbeiten + edit_answer: Antwort bearbeiten + search: Suchen + posts_containing: Beiträge enthalten + settings: Einstellungen + notifications: Benachrichtigungen + login: Anmelden + sign_up: Registrieren + account_recovery: Konto-Wiederherstellung + account_activation: Account Aktivierung + confirm_email: Bestätigungs-E-Mail + account_suspended: Konto gesperrt + admin: Verwaltung + change_email: E-Mails ändern + install: Installation beantworten + upgrade: Antwort-Upgrade + maintenance: Website-Wartung + users: Benutzer + oauth_callback: In Bearbeitung + http_404: HTTP-Fehler 404 + http_50X: HTTP-Fehler 500 + http_403: HTTP Fehler 403 + logout: Ausloggen + notifications: + title: Benachrichtigungen + inbox: Posteingang + achievement: Erfolge + new_alerts: Neue Benachrichtigungen + all_read: Alle als gelesen markieren + show_more: Mehr anzeigen + someone: Jemand + inbox_type: + all: Alle + posts: Beiträge + invites: Einladungen + votes: Abstimmungen + answer: Antwort + question: Frage + badge_award: Abzeichen + suspended: + title: Dein Konto wurde gesperrt + until_time: "Dein Konto wurde bis zum {{ time }} gesperrt." + forever: Dieser Benutzer wurde für immer gesperrt. + end: Du erfüllst keine Community-Richtlinie. + contact_us: Kontaktiere uns + editor: + blockquote: + text: Blockzitat + bold: + text: Stark + chart: + text: Bestenliste + flow_chart: Flussdiagramm + sequence_diagram: Sequenzdiagramm + class_diagram: Klassen Diagramm + state_diagram: Zustandsdiagramm + entity_relationship_diagram: Entitätsbeziehungsdiagramm + user_defined_diagram: Benutzerdefiniertes Diagramm + gantt_chart: Gantt-Diagramm + pie_chart: Kuchendiagramm + code: + text: Code Beispiel + add_code: Code-Beispiel hinzufügen + form: + fields: + code: + label: Code + msg: + empty: Code kann nicht leer sein. + language: + label: Sprache + placeholder: Automatische Erkennung + btn_cancel: Abbrechen + btn_confirm: Hinzufügen + formula: + text: Formel + options: + inline: Inline Formel + block: Block Formel + heading: + text: Überschrift + options: + h1: Überschrift 1 + h2: Überschrift 2 + h3: Überschrift 3 + h4: Überschrift 4 + h5: Überschrift 5 + h6: Überschrift 6 + help: + text: Hilfe + hr: + text: Horizontale Richtlinie + image: + text: Bild + add_image: Bild hinzufügen + tab_image: Bild hochladen + form_image: + fields: + file: + label: Bilddatei + btn: Bild auswählen + msg: + empty: Datei darf nicht leer sein. + only_image: Nur Bilddateien sind erlaubt. + max_size: Dateigröße darf {{size}} MB nicht überschreiten. + desc: + label: Beschreibung + tab_url: Bild URL + form_url: + fields: + url: + label: Bild URL + msg: + empty: Bild-URL darf nicht leer sein. + name: + label: Beschreibung + btn_cancel: Abbrechen + btn_confirm: Hinzufügen + uploading: Hochladen + indent: + text: Einzug + outdent: + text: Ausrücken + italic: + text: Hervorhebung + link: + text: Hyperlink + add_link: Hyperlink hinzufügen + form: + fields: + url: + label: URL + msg: + empty: URL darf nicht leer sein. + name: + label: Beschreibung + btn_cancel: Abbrechen + btn_confirm: Hinzufügen + ordered_list: + text: Nummerierte Liste + unordered_list: + text: Aufzählungsliste + table: + text: Tabelle + heading: Überschrift + cell: Zelle + file: + text: Datei anhängen + not_supported: "Diesen Dateityp nicht unterstützen. Versuchen Sie es erneut mit {{file_type}}." + max_size: "Dateigröße anhängen darf {{size}} MB nicht überschreiten." + close_modal: + title: Ich schließe diesen Beitrag als... + btn_cancel: Abbrechen + btn_submit: Senden + remark: + empty: Kann nicht leer sein. + msg: + empty: Bitte wähle einen Grund aus. + report_modal: + flag_title: Ich melde diesen Beitrag als... + close_title: Ich schließe diesen Beitrag wegen ... + review_question_title: Frage prüfen + review_answer_title: Antwort prüfen + review_comment_title: Kommentar prüfen + btn_cancel: Abbrechen + btn_submit: Senden + remark: + empty: Kann nicht leer sein. + msg: + empty: Bitte wähle einen Grund aus. + not_a_url: URL hat ein falsches Format. + url_not_match: URL-Ursprung stimmt nicht mit der aktuellen Website überein. + tag_modal: + title: Neuen Tag erstellen + form: + fields: + display_name: + label: Anzeigename + msg: + empty: Anzeigename darf nicht leer sein. + range: Anzeige des Namens mit bis zu 35 Zeichen. + slug_name: + label: URL-Slug + desc: 'Muss den Zeichensatz "a-z", "0-9", "+ # - " verwenden.' + msg: + empty: URL-Slug darf nicht leer sein. + range: URL-Slug mit bis zu 35 Zeichen. + character: URL-Slug enthält nicht erlaubten Zeichensatz. + desc: + label: Beschreibung + revision: + label: Version + edit_summary: + label: Zusammenfassung bearbeiten + placeholder: >- + Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) + btn_cancel: Abbrechen + btn_submit: Senden + btn_post: Neuen Tag erstellen + tag_info: + created_at: Erstellt + edited_at: Bearbeitet + history: Verlauf + synonyms: + title: Synonyme + text: Die folgenden Tags werden neu zugeordnet zu + empty: Keine Synonyme gefunden. + btn_add: Synonym hinzufügen + btn_edit: Bearbeiten + btn_save: Speichern + synonyms_text: Die folgenden Tags werden neu zugeordnet zu + delete: + title: Diesen Tag löschen + tip_with_posts: >- +

Wir erlauben es nicht, Tags mit Beiträgenzu löschen.

Bitte entfernen Sie dieses Tag zuerst aus den Beiträgen.

+ tip_with_synonyms: >- +

Wir erlauben nicht Tags mit Synonymenzu löschen.

Bitte entfernen Sie zuerst die Synonyme von diesem Schlagwort.

+ tip: Bist du sicher, dass du löschen möchtest? + close: Schließen + merge: + title: Tags zusammenführen + source_tag_title: Quell-Tag + source_tag_description: Das Quell-Tag und seine zugehörigen Daten werden dem Ziel-Tag zugeordnet. + target_tag_title: Ziel-Tag + target_tag_description: Ein Synonym zwischen diesen beiden Tags wird nach dem Zusammenführen erstellt. + no_results: Keine zusammenpassenden Tags gefunden + btn_submit: Absenden + btn_close: Schließen + edit_tag: + title: Tag bearbeiten + default_reason: Tag bearbeiten + default_first_reason: Tag hinzufügen + btn_save_edits: Änderungen speichern + btn_cancel: Abbrechen + dates: + long_date: DD. MMM + long_date_with_year: "DD. MMM YYYY" + long_date_with_time: "DD. MMM YYYY [at] HH:mm" + now: Gerade eben + x_seconds_ago: "Vor {{count}}s" + x_minutes_ago: "Vor {{count}}m" + x_hours_ago: "Vor {{count}}h" + hour: Stunde + day: tag + hours: Stunden + days: Tage + month: month + months: months + year: year + reaction: + heart: Herz + smile: Lächeln + frown: Stirnrunzeln + btn_label: Reaktionen hinzufügen oder entfernen + undo_emoji: '{{ emoji }} Reaktion rückgängig machen' + react_emoji: mit {{ emoji }} reagieren + unreact_emoji: '{{ emoji }} Reaktion entfernen' + comment: + btn_add_comment: Einen Kommentar hinzufügen + reply_to: Antwort an + btn_reply: Antwort + btn_edit: Bearbeiten + btn_delete: Löschen + btn_flag: Melden + btn_save_edits: Änderungen speichern + btn_cancel: Abbrechen + show_more: "{{count}} mehr Kommentare" + tip_question: >- + Verwende Kommentare, um nach weiteren Informationen zu fragen oder Verbesserungen vorzuschlagen. Vermeide es, Fragen in Kommentaren zu beantworten. + tip_answer: >- + Verwende Stellungsnahmen, um anderen Nutzern zu antworten oder sie über Änderungen zu informieren. Wenn du neue Informationen hinzufügst, bearbeite deinen Beitrag, anstatt zu kommentieren. + tip_vote: Es fügt dem Beitrag etwas Nützliches hinzu + edit_answer: + title: Antwort bearbeiten + default_reason: Antwort bearbeiten + default_first_reason: Antwort hinzufügen + form: + fields: + revision: + label: Version + answer: + label: Antwort + feedback: + characters: der Inhalt muss mindestens 6 Zeichen lang sein. + edit_summary: + label: Zusammenfassung bearbeiten + placeholder: >- + Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) + btn_save_edits: Änderungen speichern + btn_cancel: Abbrechen + tags: + title: Schlagwörter + sort_buttons: + popular: Beliebt + name: Name + newest: Neueste + button_follow: Folgen + button_following: Folgend + tag_label: fragen + search_placeholder: Nach Tagnamen filtern + no_desc: Der Tag hat keine Beschreibung. + more: Mehr + wiki: Wiki + ask: + title: Create Question + edit_title: Frage bearbeiten + default_reason: Frage bearbeiten + default_first_reason: Create question + similar_questions: Ähnliche Fragen + form: + fields: + revision: + label: Version + title: + label: Titel + placeholder: What's your topic? Be specific. + msg: + empty: Der Titel darf nicht leer sein. + range: Titel bis zu 150 Zeichen + body: + label: Körper + msg: + empty: Körper darf nicht leer sein. + tags: + label: Stichworte + msg: + empty: Tags dürfen nicht leer sein. + answer: + label: Antwort + msg: + empty: Antwort darf nicht leer sein. + edit_summary: + label: Zusammenfassung bearbeiten + placeholder: >- + Erkläre kurz deine Änderungen (korrigierte Rechtschreibung, korrigierte Grammatik, verbesserte Formatierung) + btn_post_question: Poste deine Frage + btn_save_edits: Änderungen speichern + answer_question: Eigene Frage beantworten + post_question&answer: Poste deine Frage und Antwort + tag_selector: + add_btn: Schlagwort hinzufügen + create_btn: Neuen Tag erstellen + search_tag: Tag suchen + hint: "Describe what your content is about, at least one tag is required." + no_result: Keine Tags gefunden + tag_required_text: Benötigter Tag (mindestens eins) + header: + nav: + question: Fragen + tag: Schlagwörter + user: Benutzer + badges: Abzeichen + profile: Profil + setting: Einstellungen + logout: Ausloggen + admin: Administrator + review: Überprüfung + bookmark: Lesezeichen + moderation: Moderation + search: + placeholder: Suchen + footer: + build_on: >- + Betrieben von <1> Apache Answer - die Open-Source-Software, die Q&A-Communities betreibt.
Made with love © {{cc}}. + upload_img: + name: Ändern + loading: wird geladen... + pic_auth_code: + title: Captcha + placeholder: Gib den Text oben ein + msg: + empty: Captcha darf nicht leer sein. + inactive: + first: >- + Du bist fast fertig! Wir haben eine Aktivierungsmail an {{mail}} geschickt. Bitte folge den Anweisungen in der Mail, um dein Konto zu aktivieren. + info: "Wenn sie nicht ankommt, überprüfe deinen Spam-Ordner." + another: >- + Wir haben dir eine weitere Aktivierungs-E-Mail an {{mail}} geschickt. Es kann ein paar Minuten dauern, bis sie ankommt; überprüfe daher deinen Spam-Ordner. + btn_name: Aktivierungs Mail erneut senden + change_btn_name: E-Mail ändern + msg: + empty: Kann nicht leer sein. + resend_email: + url_label: Bist du sicher, dass du die Aktivierungs-E-Mail erneut senden willst? + url_text: Du kannst auch den Aktivierungslink oben an den Nutzer weitergeben. + login: + login_to_continue: Anmelden, um fortzufahren + info_sign: Du verfügst noch nicht über ein Konto? Registrieren + info_login: Du hast bereits ein Konto? <1>Anmelden + agreements: Wenn du dich registrierst, stimmst du der <1>Datenschutzrichtlinie und den <3>Nutzungsbedingungen zu. + forgot_pass: Passwort vergessen? + name: + label: Name + msg: + empty: Der Name darf nicht leer sein. + range: Der Name muss zwischen 2 und 30 Zeichen lang sein. + character: 'Muss den Zeichensatz "a-z", "A-Z", "0-9", " - . _" verwenden' + email: + label: E-Mail + msg: + empty: E-Mail-Feld darf nicht leer sein. + password: + label: Passwort + msg: + empty: Passwort-Feld darf nicht leer sein. + different: Die beiden eingegebenen Passwörter stimmen nicht überein + account_forgot: + page_title: Dein Passwort vergessen + btn_name: Schicke mir eine E-Mail zur Wiederherstellung + send_success: >- + Wenn ein Konto mit {{mail}} übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst. + email: + label: E-Mail + msg: + empty: E-Mail darf nicht leer sein. + change_email: + btn_cancel: Stornieren + btn_update: E-Mail Adresse aktualisieren + send_success: >- + Wenn ein Konto mit {{mail}} übereinstimmt, solltest du in Kürze eine E-Mail mit Anweisungen erhalten, wie du dein Passwort zurücksetzen kannst. + email: + label: Neue E-Mail + msg: + empty: E-Mail darf nicht leer sein. + oauth: + connect: Mit {{ auth_name }} verbinden + remove: '{{ auth_name }} entfernen' + oauth_bind_email: + subtitle: Wiederherstellungs-E-Mail zu deinem Konto hinzufügen. + btn_update: E-Mail aktualisieren + email: + label: E-Mail + msg: + empty: E-Mail darf nicht leer sein. + modal_title: E-Mail existiert bereits. + modal_content: Diese E-Mail ist bereits registriert. Bist du sicher, dass du dich mit dem bestehenden Konto verbinden möchtest? + modal_cancel: E-Mail ändern + modal_confirm: Mit dem bestehenden Konto verbinden + password_reset: + page_title: Passwort zurücksetzen + btn_name: Setze mein Passwort zurück + reset_success: >- + Du hast dein Passwort erfolgreich geändert; du wirst zur Anmeldeseite weitergeleitet. + link_invalid: >- + Dieser Link zum Zurücksetzen des Passworts ist leider nicht mehr gültig. Vielleicht ist dein Passwort bereits zurückgesetzt? + to_login: Weiter zur Anmeldeseite + password: + label: Passwort + msg: + empty: Passwort kann nicht leer sein. + length: Die Länge muss zwischen 8 und 32 liegen + different: Die auf beiden Seiten eingegebenen Passwörter sind inkonsistent + password_confirm: + label: Neues Passwort bestätigen + settings: + page_title: Einstellungen + goto_modify: Zum Ändern + nav: + profile: Profil + notification: Benachrichtigungen + account: Konto + interface: Benutzeroberfläche + profile: + heading: Profil + btn_name: Speichern + display_name: + label: Anzeigename + msg: Anzeigename darf nicht leer sein. + msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. + username: + label: Nutzername + caption: Leute können dich als "@Benutzername" erwähnen. + msg: Benutzername darf nicht leer sein. + msg_range: Der Benutzername muss zwischen 2 und 30 Zeichen lang sein. + character: 'Muss den Zeichensatz "a-z", "0-9", " - . _" verwenden' + avatar: + label: Profilbild + gravatar: Gravatar + gravatar_text: Du kannst das Bild ändern auf + custom: Benutzerdefiniert + custom_text: Du kannst dein Bild hochladen. + default: System + msg: Bitte lade einen Avatar hoch + bio: + label: Über mich + website: + label: Webseite + placeholder: "https://example.com" + msg: Website falsches Format + location: + label: Standort + placeholder: "Stadt, Land" + notification: + heading: E-Mail-Benachrichtigungen + turn_on: Aktivieren + inbox: + label: Posteingangsbenachrichtigungen + description: Antworten auf deine Fragen, Kommentare, Einladungen und mehr. + all_new_question: + label: Alle neuen Fragen + description: Lass dich über alle neuen Fragen benachrichtigen. Bis zu 50 Fragen pro Woche. + all_new_question_for_following_tags: + label: Alle neuen Fragen für folgende Tags + description: Lass dich über neue Fragen zu folgenden Tags benachrichtigen. + account: + heading: Konto + change_email_btn: E-Mail ändern + change_pass_btn: Passwort ändern + change_email_info: >- + Wir haben eine E-Mail an diese Adresse geschickt. Bitte befolge die Anweisungen zur Bestätigung. + email: + label: E-Mail + new_email: + label: Neue E-Mail + msg: Neue E-Mail darf nicht leer sein. + pass: + label: Aktuelles Passwort + msg: Passwort kann nicht leer sein. + password_title: Passwort + current_pass: + label: Aktuelles Passwort + msg: + empty: Das aktuelle Passwort darf nicht leer sein. + length: Die Länge muss zwischen 8 und 32 liegen. + different: Die beiden eingegebenen Passwörter stimmen nicht überein. + new_pass: + label: Neues Passwort + pass_confirm: + label: Neues Passwort bestätigen + interface: + heading: Benutzeroberfläche + lang: + label: Sprache der Benutzeroberfläche + text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst. + my_logins: + title: Meine Anmeldungen + label: Melde dich mit diesen Konten an oder registriere dich auf dieser Seite. + modal_title: Login entfernen + modal_content: Bist du sicher, dass du dieses Login aus deinem Konto entfernen möchtest? + modal_confirm_btn: Entfernen + remove_success: Erfolgreich entfernt + toast: + update: Aktualisierung erfolgreich + update_password: Das Kennwort wurde erfolgreich geändert. + flag_success: Danke fürs Markieren. + forbidden_operate_self: Verboten, an sich selbst zu operieren + review: Deine Überarbeitung wird nach der Überprüfung angezeigt. + sent_success: Erfolgreich gesendet + related_question: + title: Related + answers: antworten + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Frage jemanden + desc: Lade Leute ein, von denen du glaubst, dass sie die Antwort wissen könnten. + invite: Zur Antwort einladen + add: Personen hinzufügen + search: Personen suchen + question_detail: + action: Aktion + Asked: Gefragt + asked: gefragt + update: Geändert + edit: bearbeitet + commented: kommentiert + Views: Gesehen + Follow: Folgen + Following: Folgend + follow_tip: Folge dieser Frage, um Benachrichtigungen zu erhalten + answered: beantwortet + closed_in: Abgeschlossen in + show_exist: Bestehende Frage anzeigen. + useful: Nützlich + question_useful: Es ist nützlich und klar + question_un_useful: Es ist unklar oder nicht nützlich + question_bookmark: Lesezeichen für diese Frage + answer_useful: Es ist nützlich + answer_un_useful: Es ist nicht nützlich + answers: + title: Antworten + score: Punkte + newest: Neueste + oldest: Älteste + btn_accept: Akzeptieren + btn_accepted: Akzeptiert + write_answer: + title: Deine Antwort + edit_answer: Meine existierende Antwort bearbeiten + btn_name: Poste deine Antwort + add_another_answer: Weitere Antwort hinzufügen + confirm_title: Antworten fortsetzen + continue: Weitermachen + confirm_info: >- +

Bist du sicher, dass du eine weitere Antwort hinzufügen willst?

Du könntest stattdessen den Bearbeiten-Link verwenden, um deine existierende Antwort zu verfeinern und zu verbessern.

+ empty: Antwort darf nicht leer sein. + characters: der Inhalt muss mindestens 6 Zeichen lang sein. + tips: + header_1: Danke für deine Antwort + li1_1: Bitte stelle sicher, dass du die Frage beantwortest. Gib Details an und erzähle von deiner Recherche. + li1_2: Untermauere alle Aussagen, die du erstellst, mit Referenzen oder persönlichen Erfahrungen. + header_2: Aber vermeide... + li2_1: Bitte um Hilfe, um Klarstellung oder um Antwort auf andere Antworten. + reopen: + confirm_btn: Wieder öffnen + title: Diesen Beitrag erneut öffnen + content: Bist du sicher, dass du wieder öffnen willst? + list: + confirm_btn: Liste + title: Diesen Beitrag auflisten + content: Möchten Sie diesen Beitrag wirklich in der Liste anzeigen? + unlist: + confirm_btn: Von Liste nehmen + title: Diesen Beitrag von der Liste nehmen + content: Möchten Sie diesen Beitrag wirklich aus der Liste ausblenden? + pin: + title: Diesen Beitrag anpinnen + content: Bist du sicher, dass du den Beitrag global anheften möchtest? Dieser Beitrag wird in allen Beitragslisten ganz oben erscheinen. + confirm_btn: Anheften + delete: + title: Diesen Beitrag löschen + question: >- + Wir raten davon ab, Fragen mit Antworten zu löschen, weil dadurch zukünftigen Lesern dieses Wissen vorenthalten wird.

Wiederholtes Löschen von beantworteten Fragen kann dazu führen, dass dein Konto für Fragen gesperrt wird. Bist du sicher, dass du löschen möchtest? + answer_accepted: >- +

Wir empfehlen nicht, akzeptierte Antworten zu löschen, denn dadurch wird zukünftigen Lesern dieses Wissen vorenthalten.

Das wiederholte Löschen von akzeptierten Antworten kann dazu führen, dass dein Konto für die Beantwortung gesperrt wird. Bist du sicher, dass du löschen möchtest? + other: Bist du sicher, dass du löschen möchtest? + tip_answer_deleted: Diese Antwort wurde gelöscht + undelete_title: Diesen Beitrag wiederherstellen + undelete_desc: Bist du sicher, dass du die Löschung umkehren willst? + btns: + confirm: Bestätigen + cancel: Abbrechen + edit: Bearbeiten + save: Speichern + delete: Löschen + undelete: Wiederherstellen + list: Liste + unlist: Verstecken + unlisted: Versteckt + login: Einloggen + signup: Registrieren + logout: Ausloggen + verify: Überprüfen + create: Erstellen + approve: Genehmigen + reject: Ablehnen + skip: Überspringen + discard_draft: Entwurf verwerfen + pinned: Angeheftet + all: Alle + question: Frage + answer: Antwort + comment: Kommentar + refresh: Aktualisieren + resend: Erneut senden + deactivate: Deaktivieren + active: Aktiv + suspend: Sperren + unsuspend: Entsperren + close: Schließen + reopen: Wieder öffnen + ok: Okay + light: Hell + dark: Dunkel + system_setting: System-Einstellung + default: Standard + reset: Zurücksetzen + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignorieren + submit: Absenden + normal: Normal + closed: Geschlossen + deleted: Gelöscht + deleted_permanently: Dauerhaft gelöscht + pending: Ausstehend + more: Mehr + view: Betrachten + card: Karte + compact: Kompakt + display_below: Unten anzeigen + always_display: Immer anzeigen + or: oder + back_sites: Zurück zur Website + search: + title: Suchergebnisse + keywords: Schlüsselwörter + options: Optionen + follow: Folgen + following: Folgend + counts: "{{count}} Ergebnisse" + counts_loading: "... Results" + more: Mehr + sort_btns: + relevance: Relevanz + newest: Neueste + active: Aktiv + score: Punktzahl + more: Mehr + tips: + title: Erweiterte Suchtipps + tag: "<1>[tag] Suche mit einem Tag" + user: "<1>user:username Suche nach Autor" + answer: "<1>Antworten:0 unbeantwortete Fragen" + score: "<1>score:3 Beiträge mit einer 3+ Punktzahl" + question: "<1>is:question Suchfragen" + is_answer: "<1>ist:answer Suchantworten" + empty: Wir konnten nichts finden.
Versuche es mit anderen oder weniger spezifischen Keywords. + share: + name: Teilen + copy: Link kopieren + via: Beitrag teilen über... + copied: Kopiert + facebook: Auf Facebook teilen + twitter: Auf X teilen + cannot_vote_for_self: Du kannst nicht für deinen eigenen Beitrag stimmen. + modal_confirm: + title: Fehler... + delete_permanently: + title: Endgültig löschen + content: Sind Sie sicher, dass Sie den Inhalt endgültig löschen möchten? + account_result: + success: Dein neues Konto ist bestätigt; du wirst zur Startseite weitergeleitet. + link: Weiter zur Startseite + oops: Hoppla! + invalid: Der Link, den Sie verwendet haben, funktioniert nicht mehr. + confirm_new_email: Deine E-Mail wurde aktualisiert. + confirm_new_email_invalid: >- + Dieser Bestätigungslink ist leider nicht mehr gültig. Vielleicht wurde deine E-Mail-Adresse bereits geändert? + unsubscribe: + page_title: Abonnement entfernen + success_title: Erfolgreich vom Abo abgemeldet + success_desc: Du wurdest erfolgreich aus der Abonnentenliste gestrichen und wirst keine weiteren E-Mails von uns erhalten. + link: Einstellungen ändern + question: + following_tags: Folgende Tags + edit: Bearbeiten + save: Speichern + follow_tag_tip: Folge den Tags, um deine Liste mit Fragen zu erstellen. + hot_questions: Angesagte Fragen + all_questions: Alle Fragen + x_questions: "{{ count }} Fragen" + x_answers: "{{ count }} Antworten" + x_posts: "{{ count }} Posts" + questions: Fragen + answers: Antworten + newest: Neueste + active: Aktiv + hot: Heiß + frequent: Häufig + recommend: Empfehlen + score: Punktzahl + unanswered: Unbeantwortet + modified: geändert + answered: beantwortet + asked: gefragt + closed: schließen + follow_a_tag: Einem Tag folgen + more: Mehr + personal: + overview: Übersicht + answers: Antworten + answer: antwort + questions: Fragen + question: frage + bookmarks: Lesezeichen + reputation: Ansehen + comments: Kommentare + votes: Stimmen + badges: Abzeichen + newest: Neueste + score: Punktzahl + edit_profile: Profil bearbeiten + visited_x_days: "{{ count }} Tage besucht" + viewed: Gesehen + joined: Beigetreten + comma: "," + last_login: Gesehen + about_me: Über mich + about_me_empty: "// Hallo Welt !" + top_answers: Top-Antworten + top_questions: Top-Fragen + stats: Statistiken + list_empty: Keine Beiträge gefunden.
Vielleicht möchtest du einen anderen Reiter auswählen? + content_empty: Keine Posts gefunden. + accepted: Akzeptiert + answered: antwortete + asked: gefragt + downvoted: negativ bewertet + mod_short: MOD + mod_long: Moderatoren + x_reputation: ansehen + x_votes: Stimmen erhalten + x_answers: Antworten + x_questions: Fragen + recent_badges: Neueste Abzeichen + install: + title: Installation + next: Nächste + done: Erledigt + config_yaml_error: Die Datei config.yaml kann nicht erstellt werden. + lang: + label: Bitte wähle eine Sprache + db_type: + label: Datenbank-Engine + db_username: + label: Nutzername + placeholder: wurzel + msg: Benutzername darf nicht leer sein. + db_password: + label: Passwort + placeholder: wurzel + msg: Passwort kann nicht leer sein. + db_host: + label: Datenbank-Host + placeholder: "db:3306" + msg: Datenbank-Host darf nicht leer sein. + db_name: + label: Datenbankname + placeholder: antworten + msg: Der Datenbankname darf nicht leer sein. + db_file: + label: Datenbank-Datei + placeholder: /data/answer.Weder noch + msg: Datenbankdatei kann nicht leer sein. + ssl_enabled: + label: SSL aktivieren + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL-Modus + ssl_root_cert: + placeholder: SSL-Root-Zertifikat Pfad + msg: Pfad zum Ssl-Root-Zertifikat darf nicht leer sein + ssl_cert: + placeholder: SSL-Zertifikat Pfad + msg: Pfad zum SSL-Zertifikat darf nicht leer sein + ssl_key: + placeholder: SSL-Key Pfad + msg: Der Pfad zum SSL-Key darf nicht leer sein + config_yaml: + title: config.yaml erstellen + label: Die erstellte config.yaml-Datei. + desc: >- + Du kannst die Datei <1>config.yaml manuell im Verzeichnis <1>/var/wwww/xxx/ erstellen und den folgenden Text dort einfügen. + info: Nachdem du das getan hast, klickst du auf die Schaltfläche "Weiter". + site_information: Standortinformationen + admin_account: Administratorkonto + site_name: + label: Seitenname + msg: Standortname darf nicht leer sein. + msg_max_length: Der Name der Website darf maximal 30 Zeichen lang sein. + site_url: + label: Seiten-URL + text: Die Adresse deiner Website. + msg: + empty: Die Website-URL darf nicht leer sein. + incorrect: Falsches Format der Website-URL. + max_length: Die URL der Website darf maximal 512 Zeichen lang sein. + contact_email: + label: Kontakt E-Mail + text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist. + msg: + empty: Kontakt-E-Mail kann nicht leer sein. + incorrect: Falsches Format der Kontakt-E-Mail. + login_required: + label: Privat + switch: Anmeldung erforderlich + text: Nur eingeloggte Benutzer können auf diese Community zugreifen. + admin_name: + label: Name + msg: Der Name darf nicht leer sein. + character: 'Muss den Zeichensatz "a-z", "A-Z", "0-9", " - . _" verwenden' + msg_max_length: Der Name muss zwischen 2 und 30 Zeichen lang sein. + admin_password: + label: Passwort + text: >- + Du brauchst dieses Passwort, um dich einzuloggen. Bitte bewahre es an einem sicheren Ort auf. + msg: Passwort kann nicht leer sein. + msg_min_length: Passwort muss mindestens 8 Zeichen lang sein. + msg_max_length: Das Passwort darf maximal 32 Zeichen lang sein. + admin_confirm_password: + label: "Passwort bestätigen" + text: "Bitte geben Sie Ihr Passwort erneut ein, um es zu bestätigen." + msg: "Passwortbestätigung stimmt nicht überein!" + admin_email: + label: E-Mail + text: Du brauchst diese E-Mail, um dich einzuloggen. + msg: + empty: E-Mail darf nicht leer sein. + incorrect: E-Mail falsches Format. + ready_title: Ihre Seite ist bereit + ready_desc: >- + Wenn du noch mehr Einstellungen ändern möchtest, besuche den <1>Admin-Bereich; du findest ihn im Seitenmenü. + good_luck: "Viel Spaß und viel Glück!" + warn_title: Warnung + warn_desc: >- + Die Datei <1>config.yaml existiert bereits. Wenn du einen der Konfigurationspunkte in dieser Datei zurücksetzen musst, lösche sie bitte zuerst. + install_now: Du kannst versuchen, <1>jetzt zu installieren. + installed: Bereits installiert + installed_desc: >- + Du scheinst es bereits installiert zu haben. Um neu zu installieren, lösche bitte zuerst deine alten Datenbanktabellen. + db_failed: Datenbankverbindung fehlgeschlagen + db_failed_desc: >- + Das bedeutet entweder, dass die Datenbankinformationen in deiner <1>config.yaml Datei falsch sind oder dass der Kontakt zum Datenbankserver nicht hergestellt werden konnte. Das könnte bedeuten, dass der Datenbankserver deines Hosts ausgefallen ist. + counts: + views: Ansichten + votes: Stimmen + answers: Antworten + accepted: Akzeptiert + page_error: + http_error: HTTP Fehler {{ code }} + desc_403: Du hast keine Berechtigung, auf diese Seite zuzugreifen. + desc_404: Leider existiert diese Seite nicht. + desc_50X: Der Server ist auf einen Fehler gestoßen und konnte deine Anfrage nicht vollständig abschließen. + back_home: Zurück zur Startseite + page_maintenance: + desc: "Wir werden gewartet, wir sind bald wieder da." + nav_menus: + dashboard: Dashboard + contents: Inhalt + questions: Fragen + answers: Antworten + users: Benutzer + badges: Abzeichen + flags: Meldungen + settings: Einstellungen + general: Allgemein + interface: Benutzeroberfläche + smtp: SMTP + branding: Branding + legal: Rechtliches + write: Schreiben + tos: Nutzungsbedingungen + privacy: Privatsphäre + seo: SEO + customize: Anpassen + themes: Themen + login: Anmeldung + privileges: Berechtigungen + plugins: Erweiterungen (Plugins) + installed_plugins: Installierte Plugins + apperance: Erscheinungsbild + website_welcome: Willkommen auf {{site_name}} + user_center: + login: Anmelden + qrcode_login_tip: Bitte verwende {{ agentName }}, um den QR-Code zu scannen und dich einzuloggen. + login_failed_email_tip: Anmeldung ist fehlgeschlagen. Bitte erlaube dieser App, auf deine E-Mail-Informationen zuzugreifen, bevor du es erneut versuchst. + badges: + modal: + title: Glückwunsch + content: Sie haben sich ein neues Abzeichen verdient. + close: Schließen + confirm: Abzeichen ansehen + title: Abzeichen + awarded: Verliehen + earned_×: Verdiente ×{{ number }} + ×_awarded: "verliehen {{ number }} " + can_earn_multiple: Du kannst das mehrmals verdienen. + earned: Verdient + admin: + admin_header: + title: Administrator + dashboard: + title: Dashboard + welcome: Willkommen im Admin Bereich! + site_statistics: Website-Statistiken + questions: "Fragen:" + resolved: "Belöst:" + unanswered: "Nicht beantwortet:" + answers: "Antworten:" + comments: "Kommentare:" + votes: "Stimmen:" + users: "Nutzer:" + flags: "Meldungen:" + reviews: "Rezension:" + site_health: Gesundheit der Website + version: "Version:" + https: "HTTPS:" + upload_folder: "Hochladeverzeichnis:" + run_mode: "Betriebsmodus:" + private: Privat + public: Öffentlich + smtp: "SMTP:" + timezone: "Zeitzone:" + system_info: Systeminformationen + go_version: "Go Version:" + database: "Datenbank:" + database_size: "Datenbankgröße:" + storage_used: "Verwendeter Speicher:" + uptime: "Betriebszeit:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Kontakt + forum: Forum + documents: Dokumentation + feedback: Rückmeldung + support: Unterstützung + review: Überprüfung + config: Konfig + update_to: Aktualisieren zu + latest: Aktuell + check_failed: Prüfung fehlgeschlagen + "yes": "Ja" + "no": "Nein" + not_allowed: Nicht erlaubt + allowed: Erlaubt + enabled: Aktiviert + disabled: Deaktiviert + writable: Schreibbar + not_writable: Nicht schreibbar + flags: + title: Meldungen + pending: Ausstehend + completed: Abgeschlossen + flagged: Gekennzeichnet + flagged_type: '{{ type }} gemeldet' + created: Erstellt + action: Aktion + review: Überprüfung + user_role_modal: + title: Benutzerrolle ändern zu... + btn_cancel: Abbrechen + btn_submit: Senden + new_password_modal: + title: Neues Passwort festlegen + form: + fields: + password: + label: Passwort + text: Der Nutzer wird abgemeldet und muss sich erneut anmelden. + msg: Das Passwort muss mindestens 8-32 Zeichen lang sein. + btn_cancel: Abbrechen + btn_submit: Senden + edit_profile_modal: + title: Profil bearbeiten + form: + fields: + display_name: + label: Anzeigename + msg_range: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. + username: + label: Nutzername + msg_range: Der Benutzername muss 2-30 Zeichen lang sein. + email: + label: E-Mail + msg_invalid: Ungültige E-Mail-Adresse. + edit_success: Erfolgreich bearbeitet + btn_cancel: Abbrechen + btn_submit: Absenden + user_modal: + title: Neuen Benutzer hinzufügen + form: + fields: + users: + label: Masse Benutzer hinzufügen + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Trenne "Name, E-Mail, Passwort" mit Kommas. Ein Benutzer pro Zeile. + msg: "Bitte gib die E-Mail des Nutzers ein, eine pro Zeile." + display_name: + label: Anzeigename + msg: Der Anzeigename muss zwischen 2 und 30 Zeichen lang sein. + email: + label: E-Mail + msg: Die E-Mail ist nicht gültig. + password: + label: Passwort + msg: Das Passwort muss mindestens 8-32 Zeichen lang sein. + btn_cancel: Abbrechen + btn_submit: Senden + users: + title: Benutzer + name: Name + email: E-Mail + reputation: Ansehen + created_at: Angelegt am + delete_at: Löschzeit + suspend_at: Sperrzeit + suspend_until: Suspend until + status: Status + role: Rolle + action: Aktion + change: Ändern + all: Alle + staff: Teammitglieder + more: Mehr + inactive: Inaktiv + suspended: Gesperrt + deleted: Gelöscht + normal: Normal + Moderator: Moderation + Admin: Administrator + User: Benutzer + filter: + placeholder: "Nach Namen, user:id filtern" + set_new_password: Neues Passwort festlegen + edit_profile: Profil bearbeiten + change_status: Status ändern + change_role: Rolle wechseln + show_logs: Protokolle anzeigen + add_user: Benutzer hinzufügen + deactivate_user: + title: Benutzer deaktivieren + content: Ein inaktiver Nutzer muss seine E-Mail erneut bestätigen. + delete_user: + title: Diesen Benutzer löschen + content: Bist du sicher, dass du diesen Benutzer löschen willst? Das ist dauerhaft! + remove: Ihren Inhalt entfernen + label: Alle Fragen, Antworten, Kommentare, etc. entfernen + text: Aktiviere diese Option nicht, wenn du nur das Benutzerkonto löschen möchtest. + suspend_user: + title: Diesen Benutzer sperren + content: Ein gesperrter Benutzer kann sich nicht einloggen. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Fragen + unlisted: Nicht gelistet + post: Beitrag + votes: Stimmen + answers: Antworten + created: Erstellt + status: Status + action: Aktion + change: Ändern + pending: Ausstehend + filter: + placeholder: "Filtern nach Titel, Frage:Id" + answers: + page_title: Antworten + post: Beitrag + votes: Stimmen + created: Erstellt + status: Status + action: Aktion + change: Ändern + filter: + placeholder: "Filtern nach Titel, Antwort: id" + general: + page_title: Allgemein + name: + label: Seitenname + msg: Der Site-Name darf nicht leer sein. + text: "Der Name dieser Website, wie er im Titel-Tag verwendet wird." + site_url: + label: Seiten-URL + msg: Die Website-Url darf nicht leer sein. + validate: Bitte gib eine gültige URL ein. + text: Die Adresse deiner Website. + short_desc: + label: Kurze Seitenbeschreibung + msg: Die kurze Website-Beschreibung darf nicht leer sein. + text: "Kurze Beschreibung, wie im Titel-Tag auf der Homepage verwendet." + desc: + label: Seitenbeschreibung + msg: Die Websitebeschreibung darf nicht leer sein. + text: "Beschreibe diese Seite in einem Satz, wie er im Meta Description Tag verwendet wird." + contact_email: + label: Kontakt E-Mail + msg: Kontakt-E-Mail darf nicht leer sein. + validate: Kontakt-E-Mail ist ungültig. + text: E-Mail-Adresse des Hauptkontakts, der für diese Website verantwortlich ist. + check_update: + label: Softwareaktualisierungen + text: Automatisch auf Updates prüfen + interface: + page_title: Benutzeroberfläche + language: + label: Interface Sprache + msg: Sprache der Benutzeroberfläche darf nicht leer sein. + text: Sprache der Benutzeroberfläche. Sie ändert sich, wenn du die Seite aktualisierst. + time_zone: + label: Zeitzone + msg: Die Zeitzone darf nicht leer sein. + text: Wähle eine Stadt in der gleichen Zeitzone wie du. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Von E-Mail + msg: Von E-Mail darf nicht leer sein. + text: Die E-Mail-Adresse, von der E-Mails gesendet werden. + from_name: + label: Von Name + msg: Absendername darf nicht leer sein. + text: Der Name, von dem E-Mails gesendet werden. + smtp_host: + label: SMTP-Host + msg: Der SMTP-Host darf nicht leer sein. + text: Dein Mailserver. + encryption: + label: Verschlüsselung + msg: Verschlüsselung darf nicht leer sein. + text: Für die meisten Server ist SSL die empfohlene Option. + ssl: SSL + tls: TLS + none: Keine + smtp_port: + label: SMTP-Port + msg: SMTP-Port muss Nummer 1 ~ 65535 sein. + text: Der Port zu deinem Mailserver. + smtp_username: + label: SMTP-Benutzername + msg: Der SMTP-Benutzername darf nicht leer sein. + smtp_password: + label: SMTP-Kennwort + msg: Das SMTP-Passwort darf nicht leer sein. + test_email_recipient: + label: Test-E-Mail-Empfänger + text: Gib die E-Mail-Adresse an, an die Testsendungen gesendet werden sollen. + msg: Test-E-Mail-Empfänger ist ungültig + smtp_authentication: + label: Authentifizierung aktivieren + title: SMTP-Authentifizierung + msg: Die SMTP-Authentifizierung darf nicht leer sein. + "yes": "Ja" + "no": "Nein" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo darf nicht leer sein. + text: Das Logobild oben links auf deiner Website. Verwende ein breites rechteckiges Bild mit einer Höhe von 56 und einem Seitenverhältnis von mehr als 3:1. Wenn du es leer lässt, wird der Text des Website-Titels angezeigt. + mobile_logo: + label: Mobiles Logo + text: Das Logo wird auf der mobilen Version deiner Website verwendet. Verwende ein breites rechteckiges Bild mit einer Höhe von 56. Wenn du nichts angibst, wird das Bild aus der Einstellung "Logo" verwendet. + square_icon: + label: Quadratisches Symbol + msg: Quadratisches Symbol darf nicht leer sein. + text: Bild, das als Basis für Metadatensymbole verwendet wird. Sollte idealerweise größer als 512x512 sein. + favicon: + label: Favicon + text: Ein Favicon für deine Website. Um korrekt über ein CDN zu funktionieren, muss es ein png sein. Es wird auf 32x32 verkleinert. Wenn du es leer lässt, wird das "quadratische Symbol" verwendet. + legal: + page_title: Rechtliches + terms_of_service: + label: Nutzungsbedingungen + text: "Du kannst hier Inhalte zu den Nutzungsbedingungen hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an." + privacy_policy: + label: Datenschutzbestimmungen + text: "Du kannst hier Inhalte zur Datenschutzerklärung hinzufügen. Wenn du bereits ein Dokument hast, das anderswo gehostet wird, gib hier die vollständige URL an." + external_content_display: + label: Externer Inhalt + text: "Inhalte umfassen Bilder, Videos und Medien, die von externen Websites eingebettet sind." + always_display: Externen Inhalt immer anzeigen + ask_before_display: Vor der Anzeige externer Inhalte fragen + write: + page_title: Schreiben + restrict_answer: + title: Antwort bearbeiten + label: Jeder Benutzer kann für jede Frage nur eine Antwort schreiben + text: "Schalten Sie aus, um es Benutzern zu ermöglichen, mehrere Antworten auf dieselbe Frage zu schreiben, was dazu führen kann, dass Antworten nicht im Fokus stehen." + recommend_tags: + label: Empfohlene Tags + text: "Empfohlene Tags werden standardmäßig in der Dropdown-Liste angezeigt." + msg: + contain_reserved: "empfohlene Tags dürfen keine reservierten Tags enthalten" + required_tag: + title: Benötigte Tags festlegen + label: '"Empfohlene Tags" als erforderliche Tags festlegen' + text: "Jede neue Frage muss mindestens ein Empfehlungs-Tag haben." + reserved_tags: + label: Reservierte Tags + text: "Reservierte Tags können nur vom Moderator verwendet werden." + image_size: + label: Maximale Bildgröße (MB) + text: "Die maximale Bildladegröße." + attachment_size: + label: Maximale Anhanggröße (MB) + text: "Die maximale Dateigröße für Dateianhänge." + image_megapixels: + label: Max. BildmePixel + text: "Maximale Anzahl an Megapixeln für ein Bild." + image_extensions: + label: Autorisierte Bilderweiterungen + text: "Eine Liste von Dateierweiterungen, die für die Anzeige von Bildern erlaubt sind, getrennt durch Kommata." + attachment_extensions: + label: Autorisierte Anhänge Erweiterungen + text: "Eine Liste von Dateierweiterungen, die für das Hochladen erlaubt sind, getrennt mit Kommas. WARNUNG: Erlaubt Uploads kann Sicherheitsprobleme verursachen." + seo: + page_title: SEO + permalink: + label: Dauerlink + text: Benutzerdefinierte URL-Strukturen können die Benutzerfreundlichkeit und die Vorwärtskompatibilität deiner Links verbessern. + robots: + label: robots.txt + text: Dadurch werden alle zugehörigen Site-Einstellungen dauerhaft überschrieben. + themes: + page_title: Themen + themes: + label: Themen + text: Wähle ein bestehendes Thema aus. + color_scheme: + label: Farbschema + navbar_style: + label: Hintergrundstil der Navigationsleiste + primary_color: + label: Primäre Farbe + text: Ändere die Farben, die von deinen Themes verwendet werden + css_and_html: + page_title: CSS und HTML + custom_css: + label: Benutzerdefinierte CSS + text: > + + head: + label: Kopf + text: > + + header: + label: Header + text: > + + footer: + label: Fusszeile + text: Dies wird vor eingefügt. + sidebar: + label: Seitenleiste + text: Dies wird in die Seitenleiste eingefügt. + login: + page_title: Anmeldung + membership: + title: Mitgliedschaft + label: Neuregistrierungen zulassen + text: Schalte sie ab, um zu verhindern, dass jemand ein neues Konto erstellt. + email_registration: + title: E-Mail Registrierung + label: E-Mail-Registrierung zulassen + text: Abschalten, um zu verhindern, dass jemand ein neues Konto per E-Mail erstellt. + allowed_email_domains: + title: Zugelassene E-Mail-Domänen + text: E-Mail-Domänen, bei denen die Nutzer Konten registrieren müssen. Eine Domäne pro Zeile. Wird ignoriert, wenn leer. + private: + title: Privatgelände + label: Anmeldung erforderlich + text: Nur angemeldete Benutzer können auf diese Community zugreifen. + password_login: + title: Passwort-Login + label: E-Mail-und Passwort-Login erlauben + text: "WARNUNG: Wenn du diese Option abschaltest, kannst du dich möglicherweise nicht mehr anmelden, wenn du zuvor keine andere Anmeldemethode konfiguriert hast." + installed_plugins: + title: Installierte Plugins + plugin_link: Plugins erweitern und ergänzen die Funktionalität. Du kannst Plugins im <1>Pluginverzeichnis finden. + filter: + all: Alle + active: Aktiv + inactive: Inaktiv + outdated: Veraltet + plugins: + label: Erweiterungen + text: Wähle ein bestehendes Plugin aus. + name: Name + version: Version + status: Status + action: Aktion + deactivate: Deaktivieren + activate: Aktivieren + settings: Einstellungen + settings_users: + title: Benutzer + avatar: + label: Standard-Avatar + text: Für Benutzer ohne einen eigenen Avatar. + gravatar_base_url: + label: Gravatar Base URL + text: URL der API-Basis des Gravatar-Anbieters. Wird ignoriert, wenn leer. + profile_editable: + title: Profil bearbeitbar + allow_update_display_name: + label: Benutzern erlauben, ihren Anzeigenamen zu ändern + allow_update_username: + label: Benutzern erlauben, ihren Benutzernamen zu ändern + allow_update_avatar: + label: Benutzern erlauben, ihr Profilbild zu ändern + allow_update_bio: + label: Benutzern erlauben, ihr Über mich zu ändern + allow_update_website: + label: Benutzern erlauben, ihre Website zu ändern + allow_update_location: + label: Benutzern erlauben, ihren Standort zu ändern + privilege: + title: Berechtigungen + level: + label: Benötigtes Reputations-Level + text: Wähle die für die Privilegien erforderliche Reputation aus + msg: + should_be_number: die Eingabe muss numerisch sein + number_larger_1: Zahl muss gleich oder größer als 1 sein + badges: + action: Aktion + active: Aktiv + activate: Aktivieren + all: Alle + awards: Verliehen + deactivate: Deaktivieren + filter: + placeholder: Nach Namen, Abzeichen:id filtern + group: Gruppe + inactive: Inaktiv + name: Name + show_logs: Protokolle anzeigen + status: Status + title: Abzeichen + form: + optional: (optional) + empty: kann nicht leer sein + invalid: ist ungültig + btn_submit: Speichern + not_found_props: "Erforderliche Eigenschaft {{ key }} nicht gefunden." + select: Auswählen + page_review: + review: Überprüfung + proposed: vorgeschlagen + question_edit: Frage bearbeiten + answer_edit: Antwort bearbeiten + tag_edit: Tag bearbeiten + edit_summary: Zusammenfassung bearbeiten + edit_question: Frage bearbeiten + edit_answer: Antwort bearbeiten + edit_tag: Tag bearbeiten + empty: Keine Überprüfungsaufgaben mehr übrig. + approve_revision_tip: Akzeptieren Sie diese Revision? + approve_flag_tip: Sind Sie mit diesem Bericht einverstanden? + approve_post_tip: Bestätigen Sie diesen Beitrag? + approve_user_tip: Bestätigen Sie diesen Benutzer? + suggest_edits: Änderungsvorschläge + flag_post: Beitrag melden + flag_user: Nutzer melden + queued_post: Beitrag in Warteschlange + queued_user: Benutzer in der Warteschlange + filter_label: Typ + reputation: ansehen + flag_post_type: Diesen Beitrag als {{ type }} markiert. + flag_user_type: Diesen Benutzer als {{ type }} markiert. + edit_post: Beitrag bearbeiten + list_post: Ausgestellte Beiträge + unlist_post: Versteckte Beiträge + timeline: + undeleted: ungelöscht + deleted: gelöscht + downvote: ablehnen + upvote: positiv bewerten + accept: akzeptieren + cancelled: abgebrochen + commented: kommentiert + rollback: zurückrollen + edited: bearbeitet + answered: antwortete + asked: gefragt + closed: geschlossen + reopened: wiedereröffnet + created: erstellt + pin: angeheftet + unpin: losgelöst + show: gelistet + hide: nicht gelistet + title: "Verlauf von" + tag_title: "Zeitleiste für" + show_votes: "Stimmen anzeigen" + n_or_a: Keine Angaben + title_for_question: "Zeitleiste für" + title_for_answer: "Zeitachse für die Antwort auf {{ title }} von {{ author }}" + title_for_tag: "Zeitachse für Tag" + datetime: Terminzeit + type: Typ + by: Von + comment: Kommentar + no_data: "Wir konnten nichts finden." + users: + title: Benutzer + users_with_the_most_reputation: Benutzer mit den höchsten Reputationspunkten dieser Woche + users_with_the_most_vote: Benutzer, die diese Woche am meisten gestimmt haben + staffs: Unsere Community Teammitglieder + reputation: Ansehen + votes: Stimmen + prompt: + leave_page: Bist du sicher, dass du die Seite verlassen willst? + changes_not_save: Deine Änderungen werden möglicherweise nicht gespeichert. + draft: + discard_confirm: Bist du sicher, dass du deinen Entwurf verwerfen willst? + messages: + post_deleted: Dieser Beitrag wurde gelöscht. + post_cancel_deleted: Dieser Beitrag wurde wiederhergestellt. + post_pin: Dieser Beitrag wurde angepinnt. + post_unpin: Dieser Beitrag wurde losgelöst. + post_hide_list: Dieser Beitrag wurde aus der Liste verborgen. + post_show_list: Dieser Beitrag wird in der Liste angezeigt. + post_reopen: Dieser Beitrag wurde wieder geöffnet. + post_list: Dieser Beitrag wurde angezeigt. + post_unlist: Dieser Beitrag wurde ausgeblendet. + post_pending: Dein Beitrag wartet auf eine Überprüfung. Dies ist eine Vorschau, sie wird nach der Genehmigung sichtbar sein. + post_closed: Dieser Beitrag wurde gelöscht. + answer_deleted: Diese Antwort wurde gelöscht. + answer_cancel_deleted: Diese Antwort wurde wiederhergestellt. + change_user_role: Die Rolle dieses Benutzers wurde geändert. + user_inactive: Dieser Benutzer ist bereits inaktiv. + user_normal: Dieser Benutzer ist bereits normal. + user_suspended: Dieser Nutzer wurde gesperrt. + user_deleted: Benutzer wurde gelöscht. + badge_activated: Dieses Abzeichen wurde aktiviert. + badge_inactivated: Dieses Abzeichen wurde deaktiviert. + users_deleted: Der Benutzer wurde gelöscht. + posts_deleted: Deine Frage wurde gelöscht. + answers_deleted: Deine Antwort wurde gelöscht. + copy: In die Zwischenablage kopieren + copied: Kopiert + external_content_warning: Externe Bilder/Medien werden nicht angezeigt. + + diff --git a/i18n/el_GR.yaml b/i18n/el_GR.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/el_GR.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 2d08b02ac..da34fcb16 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,170 +1,2379 @@ -base: - success: - other: "success" - unknown: - other: "unknown error" - request_format_error: - other: "request format is not valid" - unauthorized_error: - other: "unauthorized" - database_error: - other: "data server error" +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -email: - other: "email" -password: - other: "password" +# The following fields are used for back-end -email_or_password_wrong_error: &email_or_password_wrong - other: "email or password wrong" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "answer not found" - comment: - edit_without_permission: - other: "comment not allowed to edit" - not_found: - other: "comment not found" +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: List + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms email: - duplicate: - other: "email already exists" - need_to_be_verified: - other: "email should be verified" - verify_url_expired: - other: "email verified url is expired, please resend the email" - lang: - not_found: - other: "language not found" - object: - captcha_verification_failed: - other: "captcha wrong" - disallow_follow: - other: "You are not allowed to follow" - disallow_vote: - other: "You are not allowed to vote" - disallow_vote_your_self: - other: "You can't vote for your own post!" - not_found: - other: "object not found" - verification_failed: - other: "verification failed" - email_or_password_incorrect: - other: "email or password incorrect" - old_password_verification_failed: - other: "the old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous setting" + other: Email + e_mail: + other: Email + password: + other: Password + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Email and password do not match. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. question: - not_found: - other: "question not found" - rank: - fail_to_meet_the_condition: - other: "rank fail to meet the condition" - report: - handle_failed: - other: "report handle failed" - not_found: - other: "report not found" + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + deleted_title: + other: Deleted question + questions_title: + other: Questions tag: - not_found: - other: "tag not found" - theme: - not_found: - other: "theme not found" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "user not found" - suspended: - other: "user is suspended" - username_invalid: - other: "username is invalid" - username_duplicate: - other: "username is already in use" + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Edit Tag + ask_a_question: Create Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + new_alerts: New alerts + all_read: Mark all as read + show_more: Show more + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

+

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

+

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A + communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Invite People + desc: Invite people you think can answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned -question: - close: - duplicate: - name: - other: "spam" - description: - other: "This question has been asked before and already has an answer." - guideline: - name: - other: "a community-specific reason" - description: - other: "This question doesn't meet a community guideline." - multiple: - name: - other: "needs details or clarity" - description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." - other: + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General name: - other: "something else" - description: - other: "This post requires another reason not listed above." + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for the same question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as <link> + head: + label: Head + text: This will insert before </head> + header: + label: Header + text: This will insert after <body> + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "your question has been closed" - your_question_was_deleted: - other: "your question has been deleted" - your_answer_was_deleted: - other: "your answer has been deleted" - your_comment_was_deleted: - other: "your comment has been deleted" diff --git a/i18n/es_ES.yaml b/i18n/es_ES.yaml new file mode 100644 index 000000000..655846db6 --- /dev/null +++ b/i18n/es_ES.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Completado. + unknown: + other: Error desconocido. + request_format_error: + other: Formato de la solicitud inválido. + unauthorized_error: + other: No autorizado. + database_error: + other: Error en el servidor de datos. + forbidden_error: + other: Prohibido. + duplicate_request_error: + other: Solicitud duplicada. + action: + report: + other: Reportar + edit: + other: Editar + delete: + other: Eliminar + close: + other: Cerrar + reopen: + other: Reabrir + forbidden_error: + other: Prohibido. + pin: + other: Fijar + hide: + other: Retirar + unpin: + other: Desfijar + show: + other: Lista + invite_someone_to_answer: + other: Editar + undelete: + other: Recuperar + merge: + other: Merge + role: + name: + user: + other: Usuario + admin: + other: Administrador + moderator: + other: Moderador + description: + user: + other: Predeterminado sin acceso especial. + admin: + other: Dispone de acceso total al sitio web y sus ajustes. + moderator: + other: Dispone de acceso a todas las publicaciones pero no a los ajustes de administrador. + privilege: + level_1: + description: + other: Nivel 1 (reputación menor requerida para un equipo privado, grupo) + level_2: + description: + other: Nivel 2 (reputación baja requerida para una comunidad de Startup) + level_3: + description: + other: Nivel 3 (reputación alta requerida para una comunidad madura) + level_custom: + description: + other: Nivel personalizado + rank_question_add_label: + other: Hacer una pregunta + rank_answer_add_label: + other: Escribir respuesta + rank_comment_add_label: + other: Escribir comentario + rank_report_add_label: + other: Reportar + rank_comment_vote_up_label: + other: Votar comentario a favor + rank_link_url_limit_label: + other: Publica más de 2 enlaces a la vez + rank_question_vote_up_label: + other: Votar pregunta a favor + rank_answer_vote_up_label: + other: Votar respuesta a favor + rank_question_vote_down_label: + other: Votar pregunta en contra + rank_answer_vote_down_label: + other: Votar respuesta en contra + rank_invite_someone_to_answer_label: + other: Invitar a alguien a responder + rank_tag_add_label: + other: Crear nueva etiqueta + rank_tag_edit_label: + other: Editar descripción de etiqueta (revisión necesaria) + rank_question_edit_label: + other: Editar pregunta ajena (revisión necesaria) + rank_answer_edit_label: + other: Editar respuesta ajena (revisión necesaria) + rank_question_edit_without_review_label: + other: Editar pregunta ajena sin revisión + rank_answer_edit_without_review_label: + other: Editar respuesta ajena sin revisión + rank_question_audit_label: + other: Revisar ediciones de pregunta + rank_answer_audit_label: + other: Revisar ediciones de respuesta + rank_tag_audit_label: + other: Revisar ediciones de etiqueta + rank_tag_edit_without_review_label: + other: Editar descripción de etiqueta sin revisión + rank_tag_synonym_label: + other: Administrar sinónimos de etiqueta + email: + other: Correo electrónico + e_mail: + other: Correo electrónico + password: + other: Contraseña + pass: + other: Contraseña + old_pass: + other: Current password + original_text: + other: Esta publicación + email_or_password_wrong_error: + other: Contraseña o correo incorrecto. + error: + common: + invalid_url: + other: URL no válido. + status_invalid: + other: Estado inválido. + password: + space_invalid: + other: La contraseña no puede contener espacios. + admin: + cannot_update_their_password: + other: No puede modificar su contraseña. + cannot_edit_their_profile: + other: No puede modificar su perfil. + cannot_modify_self_status: + other: No puede modificar su contraseña. + email_or_password_wrong: + other: Contraseña o correo incorrecto. + answer: + not_found: + other: Respuesta no encontrada. + cannot_deleted: + other: Sin permiso para eliminar. + cannot_update: + other: Sin permiso para actualizar. + question_closed_cannot_add: + other: Las preguntas están cerradas y no pueden añadirse. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Edición del comentario no permitida. + not_found: + other: Comentario no encontrado. + cannot_edit_after_deadline: + other: El tiempo del comentario ha sido demasiado largo para modificarlo. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Correo electrónico ya en uso. + need_to_be_verified: + other: El correo debe ser verificado. + verify_url_expired: + other: La URL verificada del correo electrónico ha caducado. Por favor, vuelva a enviar el correo electrónico. + illegal_email_domain_error: + other: No está permitido el correo electrónico de ese dominio. Por favor utilice otro. + lang: + not_found: + other: Archivo de idioma no encontrado. + object: + captcha_verification_failed: + other: Captcha fallido. + disallow_follow: + other: No dispones de permiso para seguir. + disallow_vote: + other: No dispones de permiso para votar. + disallow_vote_your_self: + other: No puedes votar a tu propia publicación. + not_found: + other: Objeto no encontrado. + verification_failed: + other: Verificación fallida. + email_or_password_incorrect: + other: Contraseña o correo incorrecto. + old_password_verification_failed: + other: La verificación de la contraseña antigua falló + new_password_same_as_previous_setting: + other: La nueva contraseña es igual a la anterior. + already_deleted: + other: Esta publicación ha sido borrada. + meta: + object_not_found: + other: Meta objeto no encontrado + question: + already_deleted: + other: Esta publicación ha sido eliminada. + under_review: + other: Tu publicación está siendo revisada. Será visible una vez sea aprobada. + not_found: + other: Pregunta no encontrada. + cannot_deleted: + other: Sin permiso para eliminar. + cannot_close: + other: Sin permiso para cerrar. + cannot_update: + other: Sin permiso para actualizar. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: El rango de reputación no cumple la condición. + vote_fail_to_meet_the_condition: + other: Gracias por los comentarios. Necesitas al menos reputación {{.Rank}} para votar. + no_enough_rank_to_operate: + other: Necesitas al menos reputación {{.Rank}} para hacer esto. + report: + handle_failed: + other: Error en el manejador del reporte. + not_found: + other: Informe no encontrado. + tag: + already_exist: + other: La etiqueta ya existe. + not_found: + other: Etiqueta no encontrada. + recommend_tag_not_found: + other: La etiqueta recomendada no existe. + recommend_tag_enter: + other: Por favor, introduce al menos una de las etiquetas requeridas. + not_contain_synonym_tags: + other: No debe contener etiquetas sinónimas. + cannot_update: + other: Sin permiso para actualizar. + is_used_cannot_delete: + other: No puedes eliminar una etiqueta que está en uso. + cannot_set_synonym_as_itself: + other: No se puede establecer como sinónimo de una etiqueta la propia etiqueta. + smtp: + config_from_name_cannot_be_email: + other: El nombre no puede ser una dirección de correo electrónico. + theme: + not_found: + other: Tema no encontrado. + revision: + review_underway: + other: No se puede editar actualmente, hay una versión en la cola de revisiones. + no_permission: + other: Sin permisos para ver. + user: + external_login_missing_user_id: + other: La plataforma de terceros no proporciona un UserID único, por lo que si no puede iniciar sesión, contacte al administrador del sitio. + external_login_unbinding_forbidden: + other: Por favor añada una contraseña de inicio de sesión a su cuenta antes de eliminar este método de acceso. + email_or_password_wrong: + other: + other: Contraseña o correo incorrecto. + not_found: + other: Usuario no encontrado. + suspended: + other: El usuario ha sido suspendido. + username_invalid: + other: Nombre de usuario no válido. + username_duplicate: + other: El nombre de usuario ya está en uso. + set_avatar: + other: Fallo al actualizar el avatar. + cannot_update_your_role: + other: No puedes modificar tu propio rol. + not_allowed_registration: + other: Actualmente el sitio no está abierto para el registro. + not_allowed_login_via_password: + other: Actualmente el sitio no está abierto para iniciar sesión por contraseña. + access_denied: + other: Acceso denegado + page_access_denied: + other: No tienes acceso a esta página. + add_bulk_users_format_error: + other: "Error {{.Field}} formato cerca de '{{.Content}}' en la línea {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "El número de usuarios que añadas a la vez debe estar en el rango de 1 a {{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Lectura de configuración fallida + database: + connection_failed: + other: Conexión a la base de datos fallida + create_table_failed: + other: Creación de tabla fallida + install: + create_config_failed: + other: No es posible crear el archivo config.yaml. + upload: + unsupported_file_format: + other: Formato de archivo no soportado. + site_info: + config_not_found: + other: Configuración del sitio no encontrada. + badge: + object_not_found: + other: Insignia no encontrada + reason: + spam: + name: + other: correo no deseado + desc: + other: Esta publicación es un anuncio, o vandalismo. No es útil o relevante para el tema actual. + rude_or_abusive: + name: + other: grosero u ofensivo + desc: + other: "Alguna persona podría considerar este contenido inapropiado para una discusión respetuosa." + a_duplicate: + name: + other: un duplicado + desc: + other: Esta pregunta ha sido hecha antes y ya ha sido resuelta. + placeholder: + other: Introduce el enlace de la pregunta existente + not_a_answer: + name: + other: no es una respuesta + desc: + other: "Esto fue publicado como respuesta pero no intenta responder a la pregunta. Podría ser una edición, un comentario, otra pregunta diferente o ser eliminado por completo." + no_longer_needed: + name: + other: ya no es necesario + desc: + other: Este comentario está desactualizado, es conversacional o no es relevante a esta publicación. + something: + name: + other: otro motivo + desc: + other: Esta publicación requiere revisión del personal por otro motivo no listado arriba. + placeholder: + other: Háganos saber qué le interesa en concreto + community_specific: + name: + other: un motivo determinado de la comunidad + desc: + other: Esta pregunta no cumple con una norma comunitaria. + not_clarity: + name: + other: requiere detalles o aclaraciones + desc: + other: Esta pregunta actualmente incluye múltiples preguntas en una. Debería enfocarse en una única cuestión. + looks_ok: + name: + other: parece correcto + desc: + other: Esta publicación es buena como es y no es de baja calidad. + needs_edit: + name: + other: necesita editarse, y lo hice + desc: + other: Mejora y corrige los problemas con esta publicación personalmente. + needs_close: + name: + other: necesita cerrar + desc: + other: Una pregunta cerrada no puede responderse, pero aún se puede editar, votar y comentar. + needs_delete: + name: + other: necesita eliminación + desc: + other: Esta publicación será eliminada. + question: + close: + duplicate: + name: + other: correo no deseado + desc: + other: La pregunta ya ha sido preguntada y resuelta previamente. + guideline: + name: + other: razón específica de la comunidad + desc: + other: Esta pregunta infringe alguna norma de la comunidad. + multiple: + name: + other: necesita más detalles o aclaraciónes + desc: + other: Esta pregunta incluye múltiples preguntas en una sola. Debería centrarse únicamente en un solo tema. + other: + name: + other: otra razón + desc: + other: Esta publicación requiere otra razón no listada arriba. + operation_type: + asked: + other: preguntada + answered: + other: respondida + modified: + other: modificada + deleted_title: + other: Pregunta eliminada + questions_title: + other: Preguntas + tag: + tags_title: + other: Etiquetas + no_description: + other: La etiqueta no tiene descripción. + notification: + action: + update_question: + other: pregunta actualizada + answer_the_question: + other: pregunta respondidas + update_answer: + other: respuesta actualizada + accept_answer: + other: respuesta aceptada + comment_question: + other: pregunta comentada + comment_answer: + other: respuesta comentada + reply_to_you: + other: te ha respondido + mention_you: + other: te ha mencionado + your_question_is_closed: + other: Tu pregunta ha sido cerrada + your_question_was_deleted: + other: Tu pregunta ha sido eliminada + your_answer_was_deleted: + other: Tu respuesta ha sido eliminada + your_comment_was_deleted: + other: Tu comentario ha sido eliminado + up_voted_question: + other: pregunta votada a favor + down_voted_question: + other: pregunta votada en contra + up_voted_answer: + other: respuesta votada a favor + down_voted_answer: + other: respuesta votada en contra + up_voted_comment: + other: comentario votado a favor + invited_you_to_answer: + other: te invitó a responder + earned_badge: + other: Ha ganado la insignia "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirma tu nueva dirección de correo" + body: + other: "Confirme su nueva dirección de correo electrónico para {{.SiteName}} haciendo clic en el siguiente enlace:
\n{{.ChangeEmailUrl}}

\n\nSi no solicitó este cambio, por favor, ignore este mensaje.

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} respondió tu pregunta" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} te invitó a responder" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Es posible que conozca la respuesta.

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} comentó en tu publicación" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVerlo en {{.SiteName}}

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída.

\n\nDarse de baja" + new_question: + title: + other: "[{{.SiteName}}] Nueva pregunta: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Reestablecimiento de contraseña" + body: + other: "Se solicitó el restablecimiento de su contraseña en {{.SiteName}}.

\n\nSi no lo solicitó, puede ignorar este mensaje.

\n\nHaga clic en el siguiente enlace para generar una nueva contraseña:
\n{{.PassResetUrl}}\n

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." + register: + title: + other: "[{{.SiteName}}] Confirma tu nueva cuenta" + body: + other: "¡Bienvenido a {{.SiteName}}!

\n\nHaga clic en el siguiente enlace y active su nueva cuenta:
\n{{.RegisterUrl}}

\n\nSi no puede hacer clic en el enlace anterior, intente copiando y pegando el enlace en la barra de dirección en su navegador web.

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." + test: + title: + other: "[{{.SiteName}}] Correo de prueba" + body: + other: "Este es un mensaje de prueba.\n

\n\n--
\nNota: Este es un mensaje automático, por favor, no responda a este mensaje ya que su respuesta no será leída." + action_activity_type: + upvote: + other: votar a favor + upvoted: + other: votado a favor + downvote: + other: voto negativo + downvoted: + other: votado en contra + accept: + other: aceptar + accepted: + other: aceptado + edit: + other: editar + review: + queued_post: + other: Publicación en cola + flagged_post: + other: Publicación marcada + suggested_post_edit: + other: Ediciones sugeridas + reaction: + tooltip: + other: "{{ .Names }} y {{ .Count }} más..." + badge: + default_badges: + autobiographer: + name: + other: Autobiógrafo + desc: + other: Completó la información de su perfil. + certified: + name: + other: Certificado + desc: + other: Completó nuestro nuevo tutorial de usuario. + editor: + name: + other: Editor + desc: + other: Primer mensaje editado. + first_flag: + name: + other: Primera Denuncia + desc: + other: Primer denuncia de un post. + first_upvote: + name: + other: Primer voto favorable + desc: + other: Primera vez que le doy un 'like' a un post. + first_link: + name: + other: Primer Enlace + desc: + other: First added a link to another post. + first_reaction: + name: + other: Primera reacción + desc: + other: Primero reaccionó al post. + first_share: + name: + other: Primer Compartir + desc: + other: Primero compartió un post. + scholar: + name: + other: Erudito + desc: + other: Hecha una pregunta y aceptada una respuesta. + commentator: + name: + other: Comentador + desc: + other: Deja 5 comentarios. + new_user_of_the_month: + name: + other: Nuevo usuario del mes + desc: + other: Contribuciones pendientes en su primer mes. + read_guidelines: + name: + other: Lea los lineamientos + desc: + other: Lea las [directrices de la comunidad]. + reader: + name: + other: Lector + desc: + other: Lee cada respuesta en un tema con más de 10 respuestas. + welcome: + name: + other: Bienvenido + desc: + other: Recibió un voto a favor. + nice_share: + name: + other: Buena Compartición + desc: + other: Compartió un post con 25 visitantes únicos. + good_share: + name: + other: Buena Compartida + desc: + other: Compartió un post con 300 visitantes únicos. + great_share: + name: + other: Excelente Compartida + desc: + other: Compartió un post con 1000 visitantes únicos. + out_of_love: + name: + other: Fuera del Amor + desc: + other: Utilizó 50 votos positivos en un día. + higher_love: + name: + other: Amor Más Alto + desc: + other: Utilizó 50 votos positivos en un día 5 veces. + crazy_in_love: + name: + other: Loco(a) por el Amor + desc: + other: Utilizó 50 votos positivos en un día 20 veces. + promoter: + name: + other: Promotor + desc: + other: Invitó a un usuario. + campaigner: + name: + other: Activista + desc: + other: Invitó a 3 usuarios básicos. + champion: + name: + other: Campeón + desc: + other: Invitado 5 miembros. + thank_you: + name: + other: Gracias + desc: + other: Tiene 20 publicaciones con votos positivos y dio 10 votos positivos. + gives_back: + name: + other: Da a Cambio + desc: + other: Tiene 100 publicaciones con votos positivos y dio 100 votos positivos. + empathetic: + name: + other: Empático + desc: + other: Tiene 500 publicaciones con votos positivos y dio 1000 votos positivos. + enthusiast: + name: + other: Entusiasta + desc: + other: Visita 10 días consecutivos. + aficionado: + name: + other: Aficionado + desc: + other: Visita 100 días consecutivos. + devotee: + name: + other: Devoto + desc: + other: Visita 365 días consecutivos. + anniversary: + name: + other: Aniversario + desc: + other: Miembro activo por un año, publicó al menos una vez. + appreciated: + name: + other: Apreciación + desc: + other: Recibió 1 voto positivo en 20 puestos. + respected: + name: + other: Respetado + desc: + other: Recibió 2 voto positivo en 100 puestos. + admired: + name: + other: Admirado + desc: + other: Recibió 5 voto positivo en 300 puestos. + solved: + name: + other: Resuelto + desc: + other: Tener una respuesta aceptada. + guidance_counsellor: + name: + other: Consejero de Orientación + desc: + other: Tener 10 respuestas aceptadas. + know_it_all: + name: + other: Sabelotodo + desc: + other: Tener 50 respuestas aceptadas. + solution_institution: + name: + other: Institución de Soluciones + desc: + other: Tener 150 respuestas aceptadas. + nice_answer: + name: + other: Buena Respuesta + desc: + other: Respuesta con una puntuación de 10 o más. + good_answer: + name: + other: Excelente Respuesta + desc: + other: Respuesta con una puntuación de 25 o más. + great_answer: + name: + other: Gran Respuesta + desc: + other: Respuesta con una puntuación de 50 o más. + nice_question: + name: + other: Buena Pregunta + desc: + other: Pregunta con una puntuación de 10 o más. + good_question: + name: + other: Excelente Pregunta + desc: + other: Pregunta con una puntuación de 25 o más. + great_question: + name: + other: Gran Pregunta + desc: + other: Pregunta con una puntuación de 50 o más. + popular_question: + name: + other: Pregunta popular + desc: + other: Pregunta con 500 puntos de vista. + notable_question: + name: + other: Pregunta Notable + desc: + other: Pregunta con 1,000 vistas. + famous_question: + name: + other: Pregunta Famosa + desc: + other: Pregunta con 5,000 vistas. + popular_link: + name: + other: Enlace Popular + desc: + other: Publicado un enlace externo con 50 clics. + hot_link: + name: + other: Enlace caliente + desc: + other: Publicado un enlace externo con 300 clics. + famous_link: + name: + other: Enlace familiar + desc: + other: Publicado un enlace externo con 100 clics. + default_badge_groups: + getting_started: + name: + other: Primeros pasos + community: + name: + other: Comunidad + posting: + name: + other: Publicación +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Cómo formatear + desc: >- + + pagination: + prev: Anterior + next: Siguiente + page_title: + question: Pregunta + questions: Preguntas + tag: Etiqueta + tags: Etiquetas + tag_wiki: wiki de Etiquetas + create_tag: Crear etiqueta + edit_tag: Editar etiqueta + ask_a_question: Create Question + edit_question: Editar Pregunta + edit_answer: Editar respuesta + search: Buscar + posts_containing: Publicaciones que contienen + settings: Ajustes + notifications: Notificaciones + login: Acceder + sign_up: Registrarse + account_recovery: Recuperación de la cuenta + account_activation: Activación de la cuenta + confirm_email: Confirmar correo electrónico + account_suspended: Cuenta suspendida + admin: Administrador + change_email: Modificar correo + install: Instalación de Answer + upgrade: Actualización de Answer + maintenance: Mantenimiento del sitio web + users: Usuarios + oauth_callback: Procesando + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Cerrar sesión + notifications: + title: Notificaciones + inbox: Buzón de entrada + achievement: Logros + new_alerts: Nuevas alertas + all_read: Marcar todo como leído + show_more: Mostrar más + someone: Alguien + inbox_type: + all: Todo + posts: Publicaciones + invites: Invitaciones + votes: Votos + answer: Respuesta + question: Pregunta + badge_award: Medalla + suspended: + title: Tu cuenta ha sido suspendida + until_time: "Tu cuenta ha sido suspendida hasta el {{ time }}." + forever: Este usuario ha sido suspendido indefinidamente. + end: Has infringido alguna norma de la comunidad. + contact_us: Contáctanos + editor: + blockquote: + text: Cita + bold: + text: Negrita + chart: + text: Gráfica + flow_chart: Diagrama de flujo + sequence_diagram: Diagrama de secuencia + class_diagram: Diagrama de clase + state_diagram: Diagrama de estado + entity_relationship_diagram: Diagrama de relación de entidad + user_defined_diagram: Diagrama definido por el usuario + gantt_chart: Diagrama de Gantt + pie_chart: Grafico de torta + code: + text: Código + add_code: Añadir código + form: + fields: + code: + label: Código + msg: + empty: Código no puede estar vacío. + language: + label: Idioma + placeholder: Detección automática + btn_cancel: Cancelar + btn_confirm: Añadir + formula: + text: Fórmula + options: + inline: Fórmula en línea + block: Bloque de fórmula + heading: + text: Encabezado + options: + h1: Encabezado 1 + h2: Encabezado 2 + h3: Encabezado 3 + h4: Encabezado 4 + h5: Encabezado 5 + h6: Encabezado 6 + help: + text: Ayuda + hr: + text: Regla horizontal + image: + text: Imagen + add_image: Añadir imagen + tab_image: Subir imagen + form_image: + fields: + file: + label: Archivo de imagen + btn: Seleccionar imagen + msg: + empty: El título no puede estar vacío. + only_image: Solo se permiten archivos de imagen. + max_size: El tamaño del archivo no puede exceder {{size}} MB. + desc: + label: Descripción + tab_url: URL de la imagen + form_url: + fields: + url: + label: URL de la imagen + msg: + empty: La URL de la imagen no puede estar vacía. + name: + label: Descripción + btn_cancel: Cancelar + btn_confirm: Añadir + uploading: Subiendo + indent: + text: Sangría + outdent: + text: Quitar sangría + italic: + text: Cursiva + link: + text: Enlace + add_link: Añadir enlace + form: + fields: + url: + label: Por sus siglas en ingles (Localizador Uniforme de recursos), dirección electrónica de un sitio web + msg: + empty: La dirección no puede estar vacía. + name: + label: Descripción + btn_cancel: Cancelar + btn_confirm: Añadir + ordered_list: + text: Lista numerada + unordered_list: + text: Lista con viñetas + table: + text: Tabla + heading: Encabezado + cell: Celda + file: + text: Adjuntar archivos + not_supported: "No soporta ese tipo de archivo. Inténtalo de nuevo con {{file_type}}." + max_size: "El tamaño de los archivos adjuntos no puede exceder {{size}} MB." + close_modal: + title: Estoy cerrando este post como... + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: No puede estar en blanco. + msg: + empty: Por favor selecciona una razón. + report_modal: + flag_title: Estoy marcando para reportar este post de... + close_title: Estoy cerrando este post como... + review_question_title: Revisar pregunta + review_answer_title: Revisar respuesta + review_comment_title: Revisar comentario + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: No puede estar en blanco. + msg: + empty: Por favor selecciona una razón. + not_a_url: El formato de la URL es incorrecto. + url_not_match: El origen de la URL no coincide con el sitio web actual. + tag_modal: + title: Crear nueva etiqueta + form: + fields: + display_name: + label: Nombre público + msg: + empty: El nombre a mostrar no puede estar vacío. + range: Nombre a mostrar con un máximo de 35 caracteres. + slug_name: + label: Ruta de la URL + desc: Slug de URL de hasta 35 caracteres. + msg: + empty: URL no puede estar vacío. + range: URL slug hasta 35 caracteres. + character: La URL amigable contiene caracteres no permitidos. + desc: + label: Descripción + revision: + label: Revisión + edit_summary: + label: Editar resumen + placeholder: >- + Explica brevemente los cambios (corrección ortográfica, mejora de formato) + btn_cancel: Cancelar + btn_submit: Enviar + btn_post: Publicar nueva etiqueta + tag_info: + created_at: Creado + edited_at: Editado + history: Historial + synonyms: + title: Sinónimos + text: Las siguientes etiquetas serán reasignadas a + empty: No se encontraron sinónimos. + btn_add: Añadir un sinónimo + btn_edit: Editar + btn_save: Guardar + synonyms_text: Las siguientes etiquetas serán reasignadas a + delete: + title: Eliminar esta etiqueta + tip_with_posts: >- +

No permitimos eliminar etiquetas con publicaciones.

Primero elimine esta etiqueta de las publicaciones.

+ tip_with_synonyms: >- +

No permitimos eliminar etiqueta con sinónimos.

Primero elimine los sinónimos de esta etiqueta.

+ tip: '¿Estás seguro de que deseas borrarlo?' + close: Cerrar + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Editar etiqueta + default_reason: Editar etiqueta + default_first_reason: Añadir etiqueta + btn_save_edits: Guardar cambios + btn_cancel: Cancelar + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [a las] HH:mm" + now: ahora + x_seconds_ago: "hace {{count}}s" + x_minutes_ago: "hace {{count}}m" + x_hours_ago: "hace {{count}}h" + hour: hora + day: día + hours: horas + days: días + month: month + months: months + year: year + reaction: + heart: corazón + smile: sonrisa + frown: frown + btn_label: añadir o eliminar reacciones + undo_emoji: deshacer reacción de {{ emoji }} + react_emoji: reaccionar con {{ emoji }} + unreact_emoji: desreaccionar con {{ emoji }} + comment: + btn_add_comment: Añadir comentario + reply_to: Responder a + btn_reply: Responder + btn_edit: Editar + btn_delete: Eliminar + btn_flag: Reportar + btn_save_edits: Guardar cambios + btn_cancel: Cancelar + show_more: "{{count}} comentarios más" + tip_question: >- + Utiliza los comentarios para pedir más información o sugerir mejoras y modificaciones. Evita responder preguntas en los comentarios. + tip_answer: >- + Usa comentarios para responder a otros usuarios o notificarles de cambios. Si estás añadiendo nueva información, edita tu publicación en vez de comentar. + tip_vote: Añade algo útil a la publicación + edit_answer: + title: Editar respuesta + default_reason: Editar respuesta + default_first_reason: Añadir respuesta + form: + fields: + revision: + label: Revisión + answer: + label: Respuesta + feedback: + characters: El contenido debe tener al menos 6 caracteres. + edit_summary: + label: Editar resumen + placeholder: >- + Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado) + btn_save_edits: Guardar cambios + btn_cancel: Cancelar + tags: + title: Etiquetas + sort_buttons: + popular: Popular + name: Nombre + newest: Más reciente + button_follow: Seguir + button_following: Siguiendo + tag_label: preguntas + search_placeholder: Filtrar por nombre de etiqueta + no_desc: La etiqueta no tiene descripción. + more: Mas + wiki: Wiki + ask: + title: Create Question + edit_title: Editar pregunta + default_reason: Editar pregunta + default_first_reason: Create question + similar_questions: Preguntas similares + form: + fields: + revision: + label: Revisión + title: + label: Título + placeholder: What's your topic? Be specific. + msg: + empty: El título no puede estar vacío. + range: Título hasta 150 caracteres + body: + label: Cuerpo + msg: + empty: Cuerpo no puede estar vacío. + tags: + label: Etiquetas + msg: + empty: Se requiere al menos una etiqueta. + answer: + label: Respuesta + msg: + empty: La respuesta no puede estar vacía. + edit_summary: + label: Editar resumen + placeholder: >- + Explique brevemente sus cambios (ortografía corregida, gramática corregida, formato mejorado) + btn_post_question: Publica tu pregunta + btn_save_edits: Guardar cambios + answer_question: Responde a tu propia pregunta + post_question&answer: Publicar una pregunta y su respuesta + tag_selector: + add_btn: Añadir etiqueta + create_btn: Crear nueva etiqueta + search_tag: Buscar etiqueta + hint: "Describe what your content is about, at least one tag is required." + no_result: Ninguna etiqueta coincide + tag_required_text: Etiqueta requerida (al menos una) + header: + nav: + question: Preguntas + tag: Etiquetas + user: Usuarios + badges: Insignias + profile: Perfil + setting: Ajustes + logout: Cerrar sesión + admin: Administrador + review: Revisar + bookmark: Marcadores + moderation: Moderación + search: + placeholder: Buscar + footer: + build_on: >- + Sitio creado por <1> Apache Answer - el software libre que impulsa comunidades de Q&A.
Hecho con amor © {{cc}}. + upload_img: + name: Cambiar + loading: cargando... + pic_auth_code: + title: Captcha + placeholder: Introduce el texto anterior + msg: + empty: El Captcha no puede estar vacío. + inactive: + first: >- + ¡Casi estás listo! Te hemos enviado un correo de activación a {{mail}}. Por favor, sigue las instrucciones en el correo para activar tu cuenta. + info: "Si no te ha llegado el correo, comprueba la carpeta de SPAM." + another: >- + Te hemos enviado otro correo de activación a {{mail}}. Puede tardar algunos minutos en llegar; asegúrate de revisar tu carpeta de SPAM. + btn_name: Reenviar correo de activación + change_btn_name: Cambiar correo + msg: + empty: No puede estar en blanco. + resend_email: + url_label: '¿Estás seguro de reenviar el correo de activación?' + url_text: También puedes dar el enlace de activación de arriba al usuario. + login: + login_to_continue: Inicia sesión para continuar + info_sign: '¿No tienes cuenta? <1>Regístrate' + info_login: '¿Ya tienes una cuenta? <1>Inicia sesión' + agreements: Al registrarte, aceptas la <1>política de privacidad y los <3>términos de servicio. + forgot_pass: '¿Has olvidado la contraseña?' + name: + label: Nombre + msg: + empty: El nombre no puede estar vacío. + range: El nombre debe tener entre 2 y 30 caracteres de largo. + character: 'Debe usar el juego de caracteres "a-z", "A-Z", "0-9", " - . _"' + email: + label: Correo electrónico + msg: + empty: El correo electrónico no puede estar vacío. + password: + label: Contraseña + msg: + empty: La contraseña no puede estar vacía. + different: Las contraseñas introducidas en ambos lados no coinciden + account_forgot: + page_title: Olvidaste Tu Contraseña + btn_name: Enviadme un correo electrónico de recuperación + send_success: >- + Si existe una cuenta con el correo {{mail}}, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente. + email: + label: Correo electrónico + msg: + empty: El correo electrónico no puede estar vacío. + change_email: + btn_cancel: Cancelar + btn_update: Actualizar dirección de correo + send_success: >- + Si existe una cuenta con el correo {{mail}}, deberías de recibir un email con instrucciones sobre cómo recuperar tu contraseña próximamente. + email: + label: Nuevo correo + msg: + empty: El correo electrónico no puede estar vacío. + oauth: + connect: Conectar con {{ auth_name }} + remove: Eliminar {{ auth_name }} + oauth_bind_email: + subtitle: Añade un correo de recuperación a tu cuenta. + btn_update: Actualizar dirección de correo + email: + label: Correo + msg: + empty: El correo no puede estar vacío. + modal_title: El correo ya está en uso. + modal_content: Este correo electrónico ha sido registrado. ¿Estás seguro de conectarlo a la cuenta existente? + modal_cancel: Cambiar correo + modal_confirm: Conectarse a la cuenta existente + password_reset: + page_title: Restablecimiento de Contraseña + btn_name: Restablecer mi contraseña + reset_success: >- + Tu contraseña ha sido actualizada con éxito; vas a ser redirigido a la página de inicio de sesión. + link_invalid: >- + Lo sentimos, este enlace de restablecimiento de contraseña ya no es válido. ¿Tal vez tu contraseña ya está restablecida? + to_login: Continuar a la página de inicio de sesión + password: + label: Contraseña + msg: + empty: La contraseña no puede estar vacía. + length: La longitud debe ser de entre 8 y 32 caracteres + different: Las contraseñas introducidas en ambos lados no coinciden + password_confirm: + label: Confirmar nueva contraseña + settings: + page_title: Ajustes + goto_modify: Ir a modificar + nav: + profile: Perfil + notification: Notificaciones + account: Cuenta + interface: Interfaz + profile: + heading: Perfil + btn_name: Guardar + display_name: + label: Nombre público + msg: El nombre a mostrar no puede estar vacío. + msg_range: Display name must be 2-30 characters in length. + username: + label: Nombre de usuario + caption: La gente puede mencionarte con "@nombredeusuario". + msg: El nombre de usuario no puede estar vacío. + msg_range: Username must be 2-30 characters in length. + character: 'Debe usar el conjunto de caracteres "a-z", "0-9", " - . _"' + avatar: + label: Imagen de perfil + gravatar: Gravatar + gravatar_text: Puedes cambiar la imagen en + custom: Propia + custom_text: Puedes subir tu propia imagen. + default: Sistema + msg: Por favor, sube una imagen + bio: + label: Sobre mí + website: + label: Sitio Web + placeholder: "https://example.com" + msg: Formato del sitio web incorrecto + location: + label: Ubicación + placeholder: "Ciudad, País" + notification: + heading: Notificaciones por correo + turn_on: Activar + inbox: + label: Notificaciones de bandeja + description: Respuestas a tus preguntas, comentarios, invitaciones, y más. + all_new_question: + label: Todas las preguntas nuevas + description: Recibe notificaciones de todas las preguntas nuevas. Hasta 50 preguntas por semana. + all_new_question_for_following_tags: + label: Todas las preguntas nuevas para las etiquetas siguientes + description: Recibe notificaciones de nuevas preguntas para las etiquetas siguientes. + account: + heading: Cuenta + change_email_btn: Cambiar correo electrónico + change_pass_btn: Cambiar contraseña + change_email_info: >- + Te hemos enviado un email a esa dirección. Por favor sigue las instrucciones de confirmación. + email: + label: Correo + new_email: + label: Nuevo correo + msg: El nuevo correo no puede estar vacío. + pass: + label: Contraseña actual + msg: La contraseña no puede estar vacía. + password_title: Contraseña + current_pass: + label: Contraseña actual + msg: + empty: La contraseña actual no puede estar vacía. + length: El largo necesita estar entre 8 y 32 caracteres. + different: Las contraseñas no coinciden. + new_pass: + label: Nueva contraseña + pass_confirm: + label: Confirmar nueva contraseña + interface: + heading: Interfaz + lang: + label: Idioma de Interfaz + text: Idioma de la interfaz de usuario. Cambiará cuando actualices la página. + my_logins: + title: Mis accesos + label: Inicia sesión o regístrate en este sitio usando estas cuentas. + modal_title: Eliminar acceso + modal_content: '¿Estás seguro de querer eliminar esta sesión de tu cuenta?' + modal_confirm_btn: Eliminar + remove_success: Eliminado con éxito + toast: + update: actualización correcta + update_password: Contraseña cambiada con éxito. + flag_success: Gracias por reportar. + forbidden_operate_self: No puedes modificar tu propio usuario + review: Tu revisión será visible luego de ser aprobada. + sent_success: Enviado con éxito + related_question: + title: Related + answers: respuestas + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Personas Preguntadas + desc: Selecciona personas que creas que sepan la respuesta. + invite: Invitar a responder + add: Añadir personas + search: Buscar personas + question_detail: + action: Acción + Asked: Preguntada + asked: preguntada + update: Modificada + edit: editada + commented: comentado + Views: Visto + Follow: Seguir + Following: Siguiendo + follow_tip: Sigue esta pregunta para recibir notificaciones + answered: respondida + closed_in: Cerrado el + show_exist: Mostrar una pregunta existente. + useful: Útil + question_useful: Es útil y claro + question_un_useful: Es poco claro o no es útil + question_bookmark: Añadir esta pregunta a marcadores + answer_useful: Es útil + answer_un_useful: No es útil + answers: + title: Respuestas + score: Puntuación + newest: Más reciente + oldest: Más antiguo + btn_accept: Aceptar + btn_accepted: Aceptada + write_answer: + title: Tu respuesta + edit_answer: Editar mi respuesta existente + btn_name: Publica tu respuesta + add_another_answer: Añadir otra respuesta + confirm_title: Continuar a pregunta + continue: Continuar + confirm_info: >- +

¿Seguro que quieres añadir otra respuesta?

Puedes utilizar el enlace de edición para detallar y mejorar tu respuesta existente en su lugar.

+ empty: La respuesta no puede estar vacía. + characters: el contenido debe tener al menos 6 caracteres. + tips: + header_1: Gracias por tu respuesta + li1_1: Asegúrate de responder la pregunta. Proporciona detalles y comparte tu investigación. + li1_2: Respalda cualquier declaración que hagas con referencias o experiencia personal. + header_2: Pero evita ... + li2_1: Pedir ayuda, pedir aclaraciones, o responder a otras respuestas. + reopen: + confirm_btn: Reabrir + title: Reabrir esta publicación + content: '¿Seguro que quieres reabrir esta publicación?' + list: + confirm_btn: Lista + title: Listar esta publicación + content: '¿Estás seguro de que quieres listar?' + unlist: + confirm_btn: Deslistar + title: No listar esta publicación + content: '¿Estás seguro de que quieres dejar de listar?' + pin: + title: Fijar esta publicación + content: '¿Estás seguro de querer fijar esto globalmente? Esta publicación aparecerá por encima de todas las listas de publicaciones.' + confirm_btn: Fijar + delete: + title: Eliminar esta publicación + question: >- + No recomendamos borrar preguntas con respuestas porque esto priva a los lectores futuros de este conocimiento.

El borrado repetido de preguntas respondidas puede resultar en que tu cuenta se bloquee para hacer preguntas. ¿Estás seguro de que deseas borrarlo? + answer_accepted: >- +

No recomendamos borrar la respuesta aceptada porque esto priva a los lectores futuros de este conocimiento.

El borrado repetido de respuestas aceptadas puede resultar en que tu cuenta se bloquee para responder. ¿Estás seguro de que deseas borrarlo? + other: '¿Estás seguro de que deseas borrarlo?' + tip_answer_deleted: Esta respuesta ha sido eliminada + undelete_title: Restaurar esta publicación + undelete_desc: '¿Estás seguro de querer restaurar?' + btns: + confirm: Confirmar + cancel: Cancelar + edit: Editar + save: Guardar + delete: Eliminar + undelete: Restaurar + list: Lista + unlist: Deslistar + unlisted: Sin enumerar + login: Acceder + signup: Registrarse + logout: Cerrar sesión + verify: Verificar + create: Create + approve: Aprobar + reject: Rechazar + skip: Omitir + discard_draft: Descartar borrador + pinned: Fijado + all: Todo + question: Pregunta + answer: Respuesta + comment: Comentario + refresh: Actualizar + resend: Reenviar + deactivate: Desactivar + active: Activar + suspend: Suspender + unsuspend: Quitar suspensión + close: Cerrar + reopen: Reabrir + ok: Aceptar + light: Claro + dark: Oscuro + system_setting: Ajuste de sistema + default: Por defecto + reset: Reiniciar + tag: Etiqueta + post_lowercase: publicación + filter: Filtro + ignore: Ignorar + submit: Enviar + normal: Normal + closed: Cerrado + deleted: Eliminado + deleted_permanently: Deleted permanently + pending: Pendiente + more: Más + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Resultados de la búsqueda + keywords: Palabras claves + options: Opciones + follow: Seguir + following: Siguiendo + counts: "{{count}} Resultados" + counts_loading: "... Results" + more: Más + sort_btns: + relevance: Relevancia + newest: Más reciente + active: Activas + score: Puntuación + more: Mas + tips: + title: Consejos de búsqueda avanzada + tag: "<1>[tag] búsqueda por etiquetas" + user: "<1>user:username búsqueda por autor" + answer: "<1>answers:0 preguntas sin responder" + score: "<1>score:3 Publicaciones con un puntaje de 3 o más" + question: "<1>is:question buscar preguntas" + is_answer: "<1>is:answer buscar respuestas" + empty: No pudimos encontrar nada.
Prueba a buscar con palabras diferentes o menos específicas. + share: + name: Compartir + copy: Copiar enlace + via: Compartir vía... + copied: Copiado + facebook: Compartir en Facebook + twitter: Share to X + cannot_vote_for_self: No puedes votar tu propia publicación. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Tu nueva cuenta ha sido confirmada, serás redirigido a la página de inicio. + link: Continuar a la página de inicio + oops: '¡Ups!' + invalid: El enlace que utilizaste ya no funciona. + confirm_new_email: Tu email ha sido actualizado. + confirm_new_email_invalid: >- + Lo siento, este enlace de confirmación ya no es válido. ¿Quizás ya se haya cambiado tu correo electrónico? + unsubscribe: + page_title: Desuscribir + success_title: Desuscrito con éxito + success_desc: Ha sido eliminado con éxito de esta lista de suscriptores y no recibirá más correos electrónicos nuestros. + link: Cambiar ajustes + question: + following_tags: Etiquetas seguidas + edit: Editar + save: Guardar + follow_tag_tip: Sigue etiquetas para personalizar tu lista de preguntas. + hot_questions: Preguntas del momento + all_questions: Todas las preguntas + x_questions: "{{ count }} Preguntas" + x_answers: "{{ count }} respuestas" + x_posts: "{{ count }} Posts" + questions: Preguntas + answers: Respuestas + newest: Más reciente + active: Activo + hot: Popular + frequent: Frecuente + recommend: Recomendar + score: Puntuación + unanswered: Sin respuesta + modified: modificada + answered: respondida + asked: preguntada + closed: cerrada + follow_a_tag: Seguir una etiqueta + more: Más + personal: + overview: Información general + answers: Respuestas + answer: respuesta + questions: Preguntas + question: pregunta + bookmarks: Guardadas + reputation: Reputación + comments: Comentarios + votes: Votos + badges: Insignias + newest: Más reciente + score: Puntuación + edit_profile: Editar perfil + visited_x_days: "Visitado {{ count }} días" + viewed: Visto + joined: Unido + comma: "," + last_login: Visto + about_me: Sobre mí + about_me_empty: "// ¡Hola Mundo!" + top_answers: Mejores respuestas + top_questions: Preguntas Principales + stats: Estadísticas + list_empty: No se encontraron publicaciones.
¿Quizás le gustaría seleccionar una pestaña diferente? + content_empty: No se han encontrado publicaciones. + accepted: Aceptada + answered: respondida + asked: preguntó + downvoted: votado negativamente + mod_short: MOD + mod_long: Moderadores + x_reputation: reputación + x_votes: votos recibidos + x_answers: respuestas + x_questions: preguntas + recent_badges: Insignias recientes + install: + title: Instalación + next: Próximo + done: Hecho + config_yaml_error: No se puede crear el archivo config.yaml. + lang: + label: Elige un idioma + db_type: + label: Motor de base de datos + db_username: + label: Nombre de usuario + placeholder: raíz + msg: El nombre de usuario no puede estar vacío. + db_password: + label: Contraseña + placeholder: raíz + msg: La contraseña no puede estar vacía. + db_host: + label: Host de base de datos + placeholder: "db:3306" + msg: El host de base de datos no puede estar vacío. + db_name: + label: Nombre de base de datos + placeholder: respuesta + msg: El nombre de la base de datos no puede estar vacío. + db_file: + label: Archivo de base de datos + placeholder: /data/respuesta.db + msg: El archivo de la base de datos no puede estar vacío. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Crear config.yaml + label: El archivo config.yaml creado. + desc: >- + Puede crear el archivo <1>config.yaml manualmente en el directorio <1>/var/www/xxx/ y pegar el siguiente texto en él. + info: Después de haber hecho eso, haga clic en el botón "Siguiente". + site_information: Información del sitio + admin_account: Cuenta de administrador + site_name: + label: Nombre del sitio + msg: El nombre del sitio no puede estar vacío. + msg_max_length: El nombre del sitio tener como máximo 30 caracteres. + site_url: + label: Sitio URL + text: La dirección de su sitio. + msg: + empty: La URL del sitio no puede estar vacía. + incorrect: Formato incorrecto de la URL del sitio. + max_length: El URL del sitio debe tener como máximo 512 caracteres. + contact_email: + label: Correo electrónico de contacto + text: Dirección de correo electrónico del contacto clave responsable de este sitio. + msg: + empty: El correo electrónico de contacto no puede estar vacío. + incorrect: Formato incorrecto de correo electrónico de contacto. + login_required: + label: Privado + switch: Inicio de sesión requerido + text: Solo usuarios conectados pueden acceder a esta comunidad. + admin_name: + label: Nombre + msg: El nombre no puede estar vacío. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Contraseña + text: >- + Necesitará esta contraseña para iniciar sesión. Guárdela en un lugar seguro. + msg: La contraseña no puede estar vacía. + msg_min_length: La contraseña debe contener 8 caracteres como mínimo. + msg_max_length: La contraseña debe contener como máximo 32 caracteres. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Correo electrónico + text: Necesitará este correo electrónico para iniciar sesión. + msg: + empty: El correo electrónico no puede estar vacío. + incorrect: Correo electrónico con formato incorrecto. + ready_title: Tu sitio está listo + ready_desc: >- + Si alguna vez desea cambiar más configuraciones, visite la <1>sección de administración; encuéntrelo en el menú del sitio. + good_luck: "¡Diviértete y buena suerte!" + warn_title: Advertencia + warn_desc: >- + El archivo <1>config.yaml ya existe. Si necesita restablecer alguno de los elementos de configuración de este archivo, elimínelo primero. + install_now: Puede intentar <1>instalar ahora. + installed: Ya instalado + installed_desc: >- + Parece que ya lo has instalado. Para reinstalar, borre primero las tablas de la base de datos anterior. + db_failed: La conexión a la base de datos falló + db_failed_desc: >- + Esto significa que la información de la base de datos en tu archivo <1>config.yaml es incorrecta o que no pudo establecerse contacto con el servidor de la base de datos. Esto podría significar que el host está caído. + counts: + views: puntos de vista + votes: votos + answers: respuestas + accepted: Aceptado + page_error: + http_error: Error HTTP {{ code }} + desc_403: No tienes permiso para acceder a esta página. + desc_404: Desafortunadamente, esta página no existe. + desc_50X: Se produjo un error en el servidor y no pudo completarse tu solicitud. + back_home: Volver a la página de inicio + page_maintenance: + desc: "Estamos en mantenimiento, pronto estaremos de vuelta." + nav_menus: + dashboard: Panel + contents: Contenido + questions: Preguntas + answers: Respuestas + users: Usuarios + badges: Insignias + flags: Banderas + settings: Ajustes + general: General + interface: Interfaz + smtp: SMTP + branding: Marca + legal: Legal + write: Escribir + tos: Términos de servicio + privacy: Privacidad + seo: ESTE + customize: Personalizar + themes: Temas + login: Iniciar sesión + privileges: Privilegios + plugins: Extensiones + installed_plugins: Extensiones Instaladas + apperance: Appearance + website_welcome: Bienvenido a {{site_name}} + user_center: + login: Iniciar sesión + qrcode_login_tip: Por favor utiliza {{ agentName }} para escanear el código QR e iniciar sesión. + login_failed_email_tip: Error al iniciar sesión, por favor permite el acceso a tu información de correo de esta aplicación antes de intentar nuevamente. + badges: + modal: + title: Enhorabuena + content: Has ganado una nueva insignia. + close: Cerrar + confirm: Ver insignia + title: Insignias + awarded: Premiado + earned_×: Obtenidos ×{{ number }} + ×_awarded: "{{ number }} adjudicado" + can_earn_multiple: Puedes ganar esto varias veces. + earned: Ganado + admin: + admin_header: + title: Administrador + dashboard: + title: Panel + welcome: '¡Bienvenido a Admin!' + site_statistics: Estadísticas del sitio + questions: "Preguntas:" + resolved: "Resuelto:" + unanswered: "Sin respuesta:" + answers: "Respuestas:" + comments: "Comentarios:" + votes: "Votos:" + users: "Usuarios:" + flags: "Banderas:" + reviews: "Revisar:" + site_health: Salud del sitio + version: "Versión:" + https: "HTTPS:" + upload_folder: "Cargar carpeta:" + run_mode: "Modo de ejecución:" + private: Privado + public: Público + smtp: "SMTP:" + timezone: "Zona horaria:" + system_info: Información del sistema + go_version: "Versión de Go:" + database: "Base de datos:" + database_size: "Tamaño de la base de datos:" + storage_used: "Almacenamiento utilizado:" + uptime: "Tiempo ejecutándose:" + links: Enlaces + plugins: Extensiones + github: GitHub + blog: Blog + contact: Contacto + forum: Foro + documents: Documentos + feedback: Comentario + support: Soporte + review: Revisar + config: Configuración + update_to: Actualizar para + latest: Lo más nuevo + check_failed: Comprobación fallida + "yes": "Si" + "no": "No" + not_allowed: No permitido + allowed: Permitido + enabled: Activado + disabled: Desactivado + writable: Redactable + not_writable: No redactable + flags: + title: Banderas + pending: Pendiente + completed: Terminado + flagged: Marcado + flagged_type: Reportado {{ type }} + created: Creado + action: Acción + review: Revisar + user_role_modal: + title: Cambiar rol de usuario a... + btn_cancel: Cancelar + btn_submit: Entregar + new_password_modal: + title: Establecer nueva contraseña + form: + fields: + password: + label: Contraseña + text: El usuario será desconectado y deberá iniciar sesión nuevamente. + msg: La contraseña debe contener entre 8 y 32 caracteres de longitud. + btn_cancel: Cancelar + btn_submit: Enviar + edit_profile_modal: + title: Editar perfil + form: + fields: + display_name: + label: Nombre para mostrar + msg_range: Display name must be 2-30 characters in length. + username: + label: Nombre de usuario + msg_range: Username must be 2-30 characters in length. + email: + label: Correo electrónico + msg_invalid: Dirección de correo inválida. + edit_success: Editado exitosamente + btn_cancel: Cancelar + btn_submit: Enviar + user_modal: + title: Añadir nuevo usuario + form: + fields: + users: + label: Añadir usuarios en cantidad + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separe “nombre, correo electrónico, contraseña” con comas. Un usuario por línea. + msg: "Por favor, introduzca el correo electrónico del usuario, uno por línea." + display_name: + label: Nombre público + msg: El nombre de la pantalla debe tener entre 2 y 30 caracteres de longitud. + email: + label: Correo + msg: El correo no es válido. + password: + label: Contraseña + msg: La contraseña debe contener entre 8 y 32 caracteres de longitud. + btn_cancel: Cancelar + btn_submit: Enviar + users: + title: Usuarios + name: Nombre + email: Correo electrónico + reputation: Reputación + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Estado + role: Rol + action: Acción + change: Cambiar + all: Todo + staff: Personal + more: Más + inactive: Inactivo + suspended: Suspendido + deleted: Eliminado + normal: Normal + Moderator: Moderador + Admin: Administrador + User: Usuario + filter: + placeholder: "Filtrar por nombre, usuario:id" + set_new_password: Establecer nueva contraseña + edit_profile: Editar perfil + change_status: Cambiar Estado + change_role: Cambiar rol + show_logs: Mostrar registros + add_user: Agregar usuario + deactivate_user: + title: Desactivar usuario + content: Un usuario inactivo debe revalidar su correo electrónico. + delete_user: + title: Eliminar este usuario + content: '¿Estás seguro de que deseas eliminar este usuario? ¡Esto es permanente!' + remove: Eliminar su contenido + label: Eliminar todas las preguntas, respuestas, comentarios, etc. + text: No marque esto si solo desea eliminar la cuenta del usuario. + suspend_user: + title: Suspender a este usuario + content: Un usuario suspendido no puede iniciar sesión. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Preguntas + unlisted: No listado + post: Correo + votes: Votos + answers: Respuestas + created: Creado + status: Estado + action: Acción + change: Cambiar + pending: Pendiente + filter: + placeholder: "Filtrar por título, pregunta:id" + answers: + page_title: Respuestas + post: Correo + votes: Votos + created: Creado + status: Estado + action: Acción + change: Cambiar + filter: + placeholder: "Filtrar por título, respuesta: id" + general: + page_title: General + name: + label: Nombre del sitio + msg: El nombre del sitio no puede estar vacío. + text: "El nombre de este sitio, tal como se usa en la etiqueta del título." + site_url: + label: Sitio URL + msg: La url del sitio no puede estar vacía. + validate: Por favor introduzca un URL válido. + text: La dirección de su sitio. + short_desc: + label: Descripción breve del sitio + msg: La descripción breve del sitio no puede estar vacía. + text: "Breve descripción, tal como se usa en la etiqueta del título en la página de inicio." + desc: + label: Descripción del sitio + msg: La descripción del sitio no puede estar vacía. + text: "Describa este sitio en una oración, como se usa en la etiqueta de meta descripción." + contact_email: + label: Correo electrónico de contacto + msg: El correo electrónico de contacto no puede estar vacío. + validate: El correo electrónico de contacto no es válido. + text: Dirección de correo electrónico del contacto clave responsable de este sitio. + check_update: + label: Actualizaciones de software + text: Comprobar actualizaciones automáticamente + interface: + page_title: Interfaz + language: + label: Idioma de Interfaz + msg: El idioma de la interfaz no puede estar vacío. + text: Idioma de la interfaz de usuario. Cambiará cuando actualice la página. + time_zone: + label: Zona horaria + msg: El huso horario no puede estar vacío. + text: Elija una ciudad en la misma zona horaria que usted. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Desde correo + msg: Desde el correo electrónico no puede estar vacío. + text: La dirección de correo electrónico desde la que se envían los correos electrónicos. + from_name: + label: Desde nombre + msg: Desde el nombre no puede estar vacío. + text: El nombre desde el que se envían los correos electrónicos. + smtp_host: + label: Host SMTP + msg: El host SMTP no puede estar vacío. + text: Su servidor de correo. + encryption: + label: Cifrado + msg: El cifrado no puede estar vacío. + text: Para la mayoría de los servidores, SSL es la opción recomendada. + ssl: SSL + tls: TLS + none: Ninguno + smtp_port: + label: Puerto SMTP + msg: El puerto SMTP debe ser el número 1 ~ 65535. + text: El puerto a su servidor de correo. + smtp_username: + label: Nombre de usuario SMTP + msg: El nombre de usuario SMTP no puede estar vacío. + smtp_password: + label: Contraseña de SMTP + msg: La contraseña SMTP no puede estar vacía. + test_email_recipient: + label: Destinatarios de correo electrónico de prueba + text: Proporcione la dirección de correo electrónico que recibirá los envíos de prueba. + msg: Los destinatarios de correo electrónico de prueba no son válidos + smtp_authentication: + label: Habilitar autenticación + title: Autenticación SMTP + msg: La autenticación SMTP no puede estar vacía. + "yes": "Si" + "no": "No" + branding: + page_title: Marca + logo: + label: Logo + msg: El logotipo no puede estar vacío. + text: La imagen del logotipo en la parte superior izquierda de su sitio. Utilice una imagen rectangular ancha con una altura de 56 y una relación de aspecto superior a 3:1. Si se deja en blanco, se mostrará el texto del título del sitio. + mobile_logo: + label: Logo Móvil + text: El logotipo utilizado en la versión móvil de su sitio. Utilice una imagen rectangular ancha con una altura de 56. Si se deja en blanco, se utilizará la imagen de la configuración de "logotipo". + square_icon: + label: Icono cuadrado + msg: El icono cuadrado no puede estar vacío. + text: Imagen utilizada como base para los iconos de metadatos. Idealmente, debería ser más grande que 512x512. + favicon: + label: Icono de favoritos + text: Un favicon para su sitio. Para que funcione correctamente sobre un CDN, debe ser un png. Se cambiará el tamaño a 32x32. Si se deja en blanco, se utilizará el "icono cuadrado". + legal: + page_title: Legal + terms_of_service: + label: Términos de servicio + text: "Puede agregar términos de contenido de servicio aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí." + privacy_policy: + label: Política de privacidad + text: "Puede agregar contenido de política de privacidad aquí. Si ya tiene un documento alojado en otro lugar, proporcione la URL completa aquí." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Escribir + restrict_answer: + title: Escribir respuesta + label: Cada usuario solo puede escribir una respuesta por pregunta + text: "Desactivar para permitir a los usuarios escribir múltiples respuestas a la misma pregunta, lo que puede causar que las respuestas no estén enfocadas." + recommend_tags: + label: Etiquetas recomendadas + text: "Las etiquetas recomendadas se mostrarán en la lista desplegable por defecto." + msg: + contain_reserved: "las etiquetas recomendadas no pueden contener etiquetas reservadas" + required_tag: + title: Establecer etiquetas necesarias + label: Establecer "Etiquetas recomendadas" como etiquetas requeridas + text: "Cada nueva pregunta debe tener al menos una etiqueta de recomendación." + reserved_tags: + label: Etiquetas reservadas + text: "Las etiquetas reservadas sólo pueden ser usadas por el moderador." + image_size: + label: Tamaño máximo de la imagen (MB) + text: "Tamaño máximo de la imagen." + attachment_size: + label: Tamaño máximo del archivo adjunto (MB) + text: "El tamaño máximo de subida de archivos adjuntos." + image_megapixels: + label: Megapixels de imagen máx + text: "Número máximo de megapixels permitidos para una imagen." + image_extensions: + label: Extensiones de adjuntos autorizadas + text: "Una lista de extensiones de archivo permitidas para la visualización de imágenes, separadas con comas." + attachment_extensions: + label: Extensiones de adjuntos autorizadas + text: "Una lista de extensiones de archivo permitidas para subir, separadas con comas. ADVERTENCIA: Permitir subidas puede causar problemas de seguridad." + seo: + page_title: SEO + permalink: + label: Enlace permanente + text: Las estructuras de URL personalizadas pueden mejorar la facilidad de uso y la compatibilidad futura de sus enlaces. + robots: + label: robots.txt + text: Esto anulará permanentemente cualquier configuración del sitio relacionada. + themes: + page_title: Temas + themes: + label: Temas + text: Seleccione un tema existente. + color_scheme: + label: Esquema de color + navbar_style: + label: Navbar background style + primary_color: + label: Color primario + text: Modifica los colores usados por tus temas + css_and_html: + page_title: CSS y HTML + custom_css: + label: CSS personalizado + text: > + + head: + label: Cabeza + text: > + + header: + label: Encabezado + text: > + + footer: + label: Pie de página + text: Esto se insertará antes . + sidebar: + label: Barra lateral + text: Esto se añadirá en la barra lateral. + login: + page_title: Iniciar sesión + membership: + title: Membresía + label: Permitir registro de nuevas ceuntas + text: Desactiva esto para evitar que cualquier persona pueda crear una cuenta. + email_registration: + title: Registro de correo electrónico + label: Permitir registro de correo electrónico + text: Desactivar para evitar registros a través de correo electrónico. + allowed_email_domains: + title: Dominios de correo electrónico permitidos + text: Dominios de correo electrónico con los que los usuarios deben registrar sus cuentas. Un dominio por línea. Ignorado cuando esté vacío. + private: + title: Privado + label: Inicio de sesión requerido + text: Sólo usuarios con sesión iniciada pueden acceder a esta comunidad. + password_login: + title: Inicio de sesión con contraseña + label: Permitir inicio de sesión con correo y contraseña + text: "ADVERTENCIA: Si se desactiva, es posible que no pueda iniciar sesión si no ha configurado previamente otro método de inicio de sesión." + installed_plugins: + title: Extensiones Instaladas + plugin_link: Los plugins extienden y expanden la funcionalidad. Puede encontrar plugins en el <1>Repositorio de plugin. + filter: + all: Todos + active: Activo + inactive: Inactivo + outdated: Desactualizado + plugins: + label: Extensiones + text: Seleccione una extensión existente. + name: Nombre + version: Versión + status: Estado + action: Acción + deactivate: Desactivar + activate: Activar + settings: Ajustes + settings_users: + title: Usuarios + avatar: + label: Avatar predeterminado + text: Para usuarios sin un avatar personalizado propio. + gravatar_base_url: + label: Gravatar Base URL + text: URL de la base API del proveedor Gravatar. Ignorado cuando esté vacío. + profile_editable: + title: Perfil editable + allow_update_display_name: + label: Permitir a usuarios cambiar su nombre público + allow_update_username: + label: Permitir a los usuarios cambiar su nombre de usuario + allow_update_avatar: + label: Permitir a los usuarios cambiar su foto de perfil + allow_update_bio: + label: Permitir a los usuarios cambiar su descripción + allow_update_website: + label: Permitir a los usuarios cambiar su sitio web + allow_update_location: + label: Permitir a los usuarios cambiar su ubicación + privilege: + title: Privilegios + level: + label: Nivel de reputación requerido + text: Elegir reputación requerida para los privilegios + msg: + should_be_number: la entrada debe ser número + number_larger_1: número debe ser igual o mayor que 1 + badges: + action: Accin + active: Activo + activate: Activación + all: All + awards: Premios + deactivate: Desactivar + filter: + placeholder: Filtrar por nombre, insignia:id + group: Grupo + inactive: Inactivo + name: Nombre + show_logs: Mostrar logs + status: Status + title: Insignias + form: + optional: (opcional) + empty: no puede estar en blanco + invalid: no es válido + btn_submit: Guardar + not_found_props: "La propiedad requerida {{ key }} no se ha encontrado." + select: Seleccionar + page_review: + review: Revisar + proposed: propuesto + question_edit: Edición de preguntas + answer_edit: Edición de respuestas + tag_edit: Edición de etiquetas + edit_summary: Editar resumen + edit_question: Editar pregunta + edit_answer: Editar respuesta + edit_tag: Editar etiqueta + empty: No quedan tareas de revisión. + approve_revision_tip: '¿Aprueban ustedes esta revisión?' + approve_flag_tip: '¿Aprueban ustedes esta bandera?' + approve_post_tip: '¿Aprueban ustedes esta bandera?' + approve_user_tip: '¿Apruebas a este usuario?' + suggest_edits: Ediciones Sugeridas + flag_post: Marcar publicación + flag_user: Marcar usuario + queued_post: Publicación en cola + queued_user: Usuario en cola + filter_label: Tipo + reputation: reputación + flag_post_type: Marcada esta publicación como {{ type }}. + flag_user_type: Marcado este usuario como {{ type }}. + edit_post: Editar publicación + list_post: Listar publicación + unlist_post: Deslistar publicación + timeline: + undeleted: recuperado + deleted: eliminado + downvote: voto negativo + upvote: votar a favor + accept: aceptar + cancelled: cancelado + commented: comentado + rollback: retroceder + edited: editada + answered: contestada + asked: preguntó + closed: cerrado + reopened: reabierto + created: creado + pin: fijado + unpin: desfijado + show: listado + hide: deslistado + title: "Historial para" + tag_title: "Línea temporal para" + show_votes: "Mostrar votos" + n_or_a: N/A + title_for_question: "Línea de tiempo para" + title_for_answer: "Cronología de la respuesta a {{ title }} por {{ author }}" + title_for_tag: "Cronología de la etiqueta" + datetime: Fecha y hora + type: Tipo + by: Por + comment: Comentario + no_data: "No pudimos encontrar nada." + users: + title: Usuarios + users_with_the_most_reputation: Usuarios con el mayor puntaje de reputación esta semana + users_with_the_most_vote: Usuarios que más votaron esta semana + staffs: Nuestor equipo de la comunidad + reputation: reputación + votes: votos + prompt: + leave_page: '¿Seguro que quieres salir de la página?' + changes_not_save: Es posible que sus cambios no se guarden. + draft: + discard_confirm: '¿Está seguro de que desea descartar este borrador?' + messages: + post_deleted: Esta publicación ha sido eliminada. + post_cancel_deleted: Este publicación ha sido restaurada. + post_pin: Esta publicación ha sido fijada. + post_unpin: Esta publicación ha sido desfijada. + post_hide_list: Esta publicación ha sido ocultada de la lista. + post_show_list: Esta publicación ha sido mostrada a la lista. + post_reopen: Esta publicación ha sido reabierta. + post_list: Esta publicación ha sido listada. + post_unlist: Esta publicación ha sido retirado de la lista.. + post_pending: Su publicación está pendiente de revisión. Esto es una vista previa, será visible después de que haya sido aprobado. + post_closed: Esta publicación ha sido cerrada. + answer_deleted: Esta respuesta ha sido eliminada. + answer_cancel_deleted: Esta respuesta ha sido restaurada. + change_user_role: El rol de este usuario ha sido cambiado. + user_inactive: Este usuario ya esta inactivo. + user_normal: Este usuario ya es normal. + user_suspended: Este usuario ha sido suspendido. + user_deleted: Este usuario ha sido eliminado. + badge_activated: Esta insignia ha sido activada. + badge_inactivated: Esta insignia ha sido desactivada. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/fa_IR.yaml b/i18n/fa_IR.yaml new file mode 100644 index 000000000..c6c48472f --- /dev/null +++ b/i18n/fa_IR.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: موفق. + unknown: + other: خطای ناشناخته. + request_format_error: + other: ساختار درخواست شناخته شده نیست. + unauthorized_error: + other: دسترسی غیر مجاز. + database_error: + other: خطای سرور داده. + forbidden_error: + other: عدم اجازه دسترسی. + duplicate_request_error: + other: ارسال تکراری. + action: + report: + other: نشان + edit: + other: ویرایش + delete: + other: حذف + close: + other: بستن + reopen: + other: بازگشایی + forbidden_error: + other: عدم اجازه دسترسی. + pin: + other: سنجاق کردن + hide: + other: پنهان کردن + unpin: + other: برداشتن سنجاق + show: + other: فهرست + invite_someone_to_answer: + other: ویرایش + undelete: + other: بازگردانی حذف + merge: + other: Merge + role: + name: + user: + other: کاربر + admin: + other: ادمین + moderator: + other: مدير + description: + user: + other: پیش فرض بدون دسترسی خاص. + admin: + other: تمامی دسترسی ها را داراست. + moderator: + other: دسترسی به تمامی پست هارا داراست بجز تنظیمات ادمین. + privilege: + level_1: + description: + other: سطح ۱ (شهرت کمی نیاز هست برای تیم/گروه های خصوصی) + level_2: + description: + other: سطح ۲ (شهرت کمی نیاز هست برای انجمن های استارتاپی) + level_3: + description: + other: سطح ۳ (شهرت بالایی برای نیاز هست برای انجمن های تکمیل) + level_custom: + description: + other: سطح دلخواه + rank_question_add_label: + other: سوال بپرس + rank_answer_add_label: + other: جواب بده + rank_comment_add_label: + other: نظر بده + rank_report_add_label: + other: نشان + rank_comment_vote_up_label: + other: رای موافق + rank_link_url_limit_label: + other: بیشتر از دو لینک را هم زمان پست کنید + rank_question_vote_up_label: + other: رای موافق + rank_answer_vote_up_label: + other: رای موافق + rank_question_vote_down_label: + other: رای مخالف + rank_answer_vote_down_label: + other: رای مخالف + rank_invite_someone_to_answer_label: + other: فردی رو دعوت کنین تا جواب بدن + rank_tag_add_label: + other: ساخت تگ جدید + rank_tag_edit_label: + other: ویرایش توضیحات تگ (نیازمند بازبینی) + rank_question_edit_label: + other: ویرایش سوال دیگران (نیازمند بازبینی) + rank_answer_edit_label: + other: ویرایش جواب دیگران (نیازمند بازبینی) + rank_question_edit_without_review_label: + other: ویرایش سوال دیگران بدون نیاز به بازبینی + rank_answer_edit_without_review_label: + other: ویرایش جواب دیگران بدون نیاز به بازبینی + rank_question_audit_label: + other: بازبینی ویرایش های سوال + rank_answer_audit_label: + other: بازبینی ویرایش های جواب + rank_tag_audit_label: + other: بازبینی ویرایش های تگ + rank_tag_edit_without_review_label: + other: ویرایش توضیحات تگ بدون بازبینی + rank_tag_synonym_label: + other: مدیریت تگ های مترادف + email: + other: ایمیل + e_mail: + other: ایمیل + password: + other: رمز + pass: + other: رمز + old_pass: + other: Current password + original_text: + other: پست جاری + email_or_password_wrong_error: + other: ایمیل و رمز وارد شده صحیح نیست. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: رمز عبور نمی تواند شامل فضای خالی باشد. + admin: + cannot_update_their_password: + other: نمیتوانید رمز عبور خود را تغییر دهید. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: نمیتوانید وضعیت خود را تغییر دهید. + email_or_password_wrong: + other: ایمیل و رمز وارد شده صحیح نیست. + answer: + not_found: + other: جواب پیدا نشد. + cannot_deleted: + other: اجازه حذف ندارید. + cannot_update: + other: اجازه بروزرسانی ندارید. + question_closed_cannot_add: + other: سوالات بسته شده اند و نمیتوان سوالی اضافه کرد. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: نظرات قابل ویرایش نیستند. + not_found: + other: نظر پیدا نشد. + cannot_edit_after_deadline: + other: زمان زیادی برای ویرایش نظر گذشته است. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: ایمیل تکراری. + need_to_be_verified: + other: ایمیل باید تایید شود. + verify_url_expired: + other: لینک تایید ایمیل منقضی شده است،‌لطفا دوباره تلاش کنید. + illegal_email_domain_error: + other: دامنه ایمیل پیشتیبانی نمی شود، لطفا از ایمیل دیگری استفاده کنید. + lang: + not_found: + other: فایل زبان یافت نشد. + object: + captcha_verification_failed: + other: اشتباه در Captcha. + disallow_follow: + other: شما اجازه فالو کردن ندارید. + disallow_vote: + other: شما اجازه رای دادن ندارید. + disallow_vote_your_self: + other: شما نمی توانید به پست خودتان رای دهید. + not_found: + other: آبجکت مورد نظر پیدا نشد. + verification_failed: + other: تایید با خطا مواجه شد. + email_or_password_incorrect: + other: ایمیل و رمز وارد شده صحیح نیست. + old_password_verification_failed: + other: پسورد قدیمی تایید نشد + new_password_same_as_previous_setting: + other: پسورد جدید با پسورد قدیمی یکسان است. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: این پست حذف شده است. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: سوال پیدا نشد. + cannot_deleted: + other: اجازه حذف ندارید. + cannot_close: + other: اجاره بستن ندارید. + cannot_update: + other: اجازه بروزرسانی ندارید. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: شهرت ناکافی. + vote_fail_to_meet_the_condition: + other: ممنون بابت بازخورد. شما حداقل به {{.Rank}} نیاز دارید برای رای دادن. + no_enough_rank_to_operate: + other: شما حداقل به {{.Rank}} نیاز دارید برای انجام این کار. + report: + handle_failed: + other: گزارش دهی با مشکل مواجه شد. + not_found: + other: گزارش مورد نظر پیدا نشد. + tag: + already_exist: + other: تگ از قبل موجود است. + not_found: + other: تگ پیدا نشد. + recommend_tag_not_found: + other: تگ پیشنهاد شده موجود نیست. + recommend_tag_enter: + other: لطفا حداقل یک تگ را وارد کنید. + not_contain_synonym_tags: + other: نباید تگ مترادف داشته باشد. + cannot_update: + other: اجازه بروزرسانی ندارید. + is_used_cannot_delete: + other: نمی توانید تگی که در حال استفاده است را حذف کنید. + cannot_set_synonym_as_itself: + other: شما نمی توانید مترادفی برای برچسب فعلی به عوان خودش تنظیم کنین. + smtp: + config_from_name_cannot_be_email: + other: '"از طرفه" نمی تواند آدرس ایمیل باشد.' + theme: + not_found: + other: تم پیدا نشد. + revision: + review_underway: + other: فعلا امکان ویرایش وجود ندارد،‌این نسخه در صف بازینی قرار دارد. + no_permission: + other: اجاره بازبینی و اصلاح ندارید. + user: + external_login_missing_user_id: + other: پلتفورم های سوم شخص نمی توانند نام کاربر خاصی را ارائه دهند، بنابر این شما نمی توانید وارد شوید، لطفا با مدیریت وبسایت تماس بگیرید. + external_login_unbinding_forbidden: + other: لطفاً یک رمز ورود برای حساب خود قبل از حذف تنظیم کنید. + email_or_password_wrong: + other: + other: ایمیل و رمز وارد شده صحیح نیست. + not_found: + other: کاربر پیدا نشد. + suspended: + other: کاربر در حالت تعلیق قرار داده شده است. + username_invalid: + other: نام کاربری نامعتبر است. + username_duplicate: + other: این نام کاربری قبلا استفاده شده است. + set_avatar: + other: ست کردن آواتار با مشکل مواجه شد. + cannot_update_your_role: + other: شما نمی توانید وظیفه خود را تغییر دهید. + not_allowed_registration: + other: درحال حاضر سایت برای ثبت نام باز نیست. + not_allowed_login_via_password: + other: در حال حاضر سایت اجازه ورود از طریق رمز عبور ندارد. + access_denied: + other: دسترسی مجاز نیست + page_access_denied: + other: شما وجوز دسترسی به این صفحه را ندارید. + add_bulk_users_format_error: + other: "مشکل پیش آمده در فرمت {{.Field}} در کنار {{.Content}} در خط {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "تعداد کاربرانی که اضافه می کنید باید رنج بین ۱-{{.MaxAmount}} باشند." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: خواندن کافیگ با مشکل مواجه شد + database: + connection_failed: + other: اتصال به دیتابیس موفقیت آمیز نبود + create_table_failed: + other: ایجاد کردن جدول موفقیت آمیز نبود + install: + create_config_failed: + other: فایل config.yaml نمی تواند ایجاد شود. + upload: + unsupported_file_format: + other: فرمت فایل پشتیبانی نمی شود. + site_info: + config_not_found: + other: پیکربندی سایت پیدا نشد. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: هرزنامه + desc: + other: این پست یک تبلیغ یا خرابکاری است. این پس مفید یا مربوط به این موضوع نمی باشد. + rude_or_abusive: + name: + other: بی ادب یا توهین آمیز + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: تکراری + desc: + other: این سوال قبلا پرسیده و جواب داده شده است. + placeholder: + other: لینک سوال مورد نظر را وارد کنید + not_a_answer: + name: + other: این یک پاسخ نیست + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: دیگر نیازی نیست + desc: + other: این نظر منسوخ شده، مکلامه ای یا مربوط به این پس نیست. + something: + name: + other: یک مورد دیگر + desc: + other: این پست به دلیل دیگری که در بالا ذکر نشده نیاز به توجه کارکنان دارد. + placeholder: + other: به طور خاص به ما اطلاع دهید که در مورد چه چیزی نگران هستید + community_specific: + name: + other: یک دلیل خاص جامعه + desc: + other: این سوال با دستورالعمل جامعه مطابقت ندارد. + not_clarity: + name: + other: نیاز به جزئیات یا واضح کردن دارد + desc: + other: این سوال درحال حاضر شامل چندتا سوال در یکی هست. باید فقط روی یک مشکل تمرکز کند. + looks_ok: + name: + other: به نظر خوب میاد + desc: + other: این پست همانطور که هست خوب است و کیفیت پایینی ندارد. + needs_edit: + name: + other: نیاز به ویرایش بود، من انجام دادم + desc: + other: مشکلات این پست را خودتان بهبود و اصلاح کنید. + needs_close: + name: + other: نیاز است که بسته بشود + desc: + other: به یک سوال بسته شده نمیتوان جوابی ثبت کرد بلکه می توان ویرایش، رای و نظر داد. + needs_delete: + name: + other: نیاز است که حذف بشود + desc: + other: این پست حذف خواهد شد. + question: + close: + duplicate: + name: + other: هرزنامه + desc: + other: این سوال قبلا پرسیده و جواب داده شده است. + guideline: + name: + other: یک دلیل خاص جامعه + desc: + other: این سوال با دستورالعمل جامعه مطابقت ندارد. + multiple: + name: + other: نیاز به جزئیات یا واضح کردن دارد + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: یک مورد دیگر + desc: + other: این پست به دلیل دیگری نیاز دارد که در بالا ذکر نشده است. + operation_type: + asked: + other: پرسیده شده + answered: + other: جواب داده + modified: + other: تغییر یافته + deleted_title: + other: سوال حذف شده + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: سوال بارگزاری شده + answer_the_question: + other: سؤال جواب داده شده + update_answer: + other: جواب بارگذاری شده + accept_answer: + other: جواب پذیرفته شده + comment_question: + other: سوال از کامنت + comment_answer: + other: جواب از کامنت + reply_to_you: + other: به شما پاسخ داد + mention_you: + other: به شما اشاره کرده + your_question_is_closed: + other: سوال شما بسته شده است + your_question_was_deleted: + other: سوال شما حذف شده است + your_answer_was_deleted: + other: جواب شما حذف شده است + your_comment_was_deleted: + other: نظر شما پاک شده است + up_voted_question: + other: رای موافق + down_voted_question: + other: سوال با رای منفی + up_voted_answer: + other: پاسخ موافق + down_voted_answer: + other: جواب مخالف + up_voted_comment: + other: نظر بدون رای + invited_you_to_answer: + other: برای جواب دادن دعوت شده اید + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "آدرس ایمیل جدید خود را تایید کنید{{.SiteName}}" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} به سؤال شما پاسخ داد" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} شما را به پاسخ دعوت کرد" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} روی پست شما نظر داد" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] سؤال جدید: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] گذرواژه بازنشانی شد" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] حساب کاربری جدید خود را تأیید کنید" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] ایمیل آزمایشی" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: رأی مثبت + upvoted: + other: رأی مثبت + downvote: + other: رأی منفی + downvoted: + other: رأی منفی + accept: + other: پذیرفتن + accepted: + other: پذیرفته شده + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: نحوه فرمت کردن + desc: >- + + pagination: + prev: قبلی + next: بعدی + page_title: + question: سوال + questions: سوالات + tag: برچسب + tags: برچسب ها + tag_wiki: ویکی تگ + create_tag: ایجاد برچسب + edit_tag: ویرایش برچسب + ask_a_question: Create Question + edit_question: ویرایش سوال + edit_answer: ویرایش پاسخ + search: جستجو + posts_containing: پست های شامل + settings: تنظیمات + notifications: اعلانات + login: ورود + sign_up: ثبت نام + account_recovery: بازیابی حساب کاربری + account_activation: فعالسازی حساب + confirm_email: تایید ایمیل + account_suspended: حساب تعلیق شد + admin: ادمین + change_email: نگارش ایمیل + install: نصب Bepors + upgrade: بروزرسانی بپرس + maintenance: تعمیر و نگهداری وب سایت + users: کاربرها + oauth_callback: در حال پردازش + http_404: خطای 404 HTTP + http_50X: خطای 500 HTTP + http_403: خطای 403 HTTP + logout: خروج + notifications: + title: اعلانات + inbox: پیغام‌های دریافتی + achievement: دستاوردها + new_alerts: هشدار جدید + all_read: علامتگذاری همه بعنوان خوانده شده + show_more: نمایش بیشتر + someone: کسی + inbox_type: + all: همه + posts: پست ها + invites: دعوت ها + votes: آراء + answer: Answer + question: Question + badge_award: Badge + suspended: + title: حساب شما معلق شده است + until_time: "حساب شما تا تاریخ {{ time }} به حالت تعلیق درآمده است." + forever: این کاربر برای همیشه به حالت تعلیق درآمده است. + end: شما یک دستورالعمل انجمن را رعایت نمی کنید. + contact_us: ارتباط با ما + editor: + blockquote: + text: مسابقه + bold: + text: قوی + chart: + text: نمودار + flow_chart: نمودار جریان + sequence_diagram: نمودار توالی + class_diagram: نمودار کلاس + state_diagram: نمودار حالت + entity_relationship_diagram: نمودار رابطه موجودیت + user_defined_diagram: نمودار تعریف شده توسط کاربر + gantt_chart: نمودار گانت + pie_chart: نمودار دابره‌ای + code: + text: نمونه کد + add_code: نمونه کد اضافه کنید + form: + fields: + code: + label: کد + msg: + empty: کد نمی تواند خالی باشد. + language: + label: زبان + placeholder: تشخیص خودکار + btn_cancel: لغو + btn_confirm: اضافه کردن + formula: + text: فرمول + options: + inline: فرمول در خط + block: بلاک کردن فرمول + heading: + text: سرفصل + options: + h1: سرفصل ۱ + h2: سرفصل ۲ + h3: سرفصل ۳ + h4: سرفصل ۴ + h5: سرفصل ۵ + h6: سرفصل ۶ + help: + text: راهنما + hr: + text: خط افقی + image: + text: عکس + add_image: افزودن عکس + tab_image: آپلود عکس + form_image: + fields: + file: + label: فایل عکس + btn: انتخاب عکس + msg: + empty: فایل نمی تواند خالی باشد. + only_image: فقط فایل های تصویری مجاز هستند. + max_size: File size cannot exceed {{size}} MB. + desc: + label: توضیحات + tab_url: لینک عکس + form_url: + fields: + url: + label: لینک عکس + msg: + empty: آدرس عکس نمی‌تواند خالی باشد. + name: + label: توضیحات + btn_cancel: لغو + btn_confirm: اضافه کردن + uploading: درحال ارسال + indent: + text: تورفتگی + outdent: + text: بیرون آمدگی + italic: + text: تاکید + link: + text: فراپیوند + add_link: اضافه کردن فراپیوند + form: + fields: + url: + label: آدرس + msg: + empty: آدرس نمی‌تواند خالی باشد. + name: + label: توضیح + btn_cancel: لغو + btn_confirm: افزودن + ordered_list: + text: فهرست عددی + unordered_list: + text: لیست گلوله‌ای + table: + text: جدول + heading: سرفصل + cell: تلفن همراه + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: این پست را می بندم بدلیل... + btn_cancel: لغو + btn_submit: فرستادن + remark: + empty: نمی‌تواند خالی باشد. + msg: + empty: لطفا یک دلیل را انتخاب کنید. + report_modal: + flag_title: من پرچم گذاری می کنم تا این پست را به عنوان گزارش کنم... + close_title: این پست را می بندم بدلیل... + review_question_title: بازبینی سوال + review_answer_title: بازبینی جواب + review_comment_title: بازبینی نظر + btn_cancel: لغو + btn_submit: ثبت + remark: + empty: نمی‌تواند خالی باشد. + msg: + empty: لطفا یک دلیل را انتخاب کنید. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: ساخت تگ جدید + form: + fields: + display_name: + label: نام + msg: + empty: نام نمی تواند خالی باشد. + range: نام باید نهایتا ۳۵ حرف داشته باشد. + slug_name: + label: نامک آدس + desc: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد. + msg: + empty: نامک آدرس نمی تواند خالی باشد. + range: نامک آدرس باید نهایتا ۳۵ حرف داشته باشد. + character: نامک آدرس شامل کلمات غیر مجاز می باشد. + desc: + label: توضیحات + revision: + label: تجدید نظر + edit_summary: + label: ویرایش خلاصه + placeholder: >- + تغییرات خود را به طور خلاصه توضیح دهید (املا صحیح، دستور زبان مناسب، قالب بندی بهبود یافته) + btn_cancel: لغو + btn_submit: ثبت + btn_post: پست کردن تگ جدید + tag_info: + created_at: ایجاد شده + edited_at: ویرایش شده + history: تاریخچه + synonyms: + title: مترادف ها + text: تگ های زیر مجدداً به آنها نگاشت می شوند + empty: مترادفی پیدا نشد. + btn_add: افزودن مترادف + btn_edit: ویرایش + btn_save: ذخیره + synonyms_text: تگ های زیر مجدداً به آنها نگاشت می شوند + delete: + title: این برچسب حذف شود + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: مطمئنید که میخواهید حذف شود? + close: بستن + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: ویرایش تگ‌ + default_reason: ویرایش تگ‌ + default_first_reason: برچسب اضافه کنید + btn_save_edits: ذخیره ویرایشها + btn_cancel: لغو + dates: + long_date: ماه ماه ماه روز + long_date_with_year: "ماه روز، سال" + long_date_with_time: "ماه روز، سال در ساعت:دقیقه" + now: حالا + x_seconds_ago: "{{count}} ثانیه پیش" + x_minutes_ago: "{{count}} دقیقه پیش" + x_hours_ago: "{{count}}ساعت پیش" + hour: ساعت + day: روز + hours: ساعات + days: روزها + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: افزودن نظر + reply_to: پاسخ به + btn_reply: پاسخ + btn_edit: ویرایش + btn_delete: حذف + btn_flag: نشان + btn_save_edits: ذخیره ویرایشها + btn_cancel: لغو + show_more: "{{count}} نظر بیشتر" + tip_question: >- + از نظرات برای درخواست اطلاعات بیشتر یا پیشنهاد بهبود استفاده کنید. از پاسخ دادن به سوالات در نظرات خودداری کنید. + tip_answer: >- + از نظرات برای پاسخ دادن به سایر کاربران یا اطلاع دادن آنها از تغییرات استفاده کنید. اگر اطلاعات جدیدی اضافه می کنید، به جای نظر دادن، پست خود را ویرایش کنید. + tip_vote: چیز مفیدی به پست اضافه می کند + edit_answer: + title: ویرایش جواب + default_reason: ویرایش جواب + default_first_reason: پاسخ را اضافه کنید + form: + fields: + revision: + label: بازنگری + answer: + label: پاسخ + feedback: + characters: متن باید حداقل ۶ حرف داشته باشد. + edit_summary: + label: ویرایش خلاصه + placeholder: >- + بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی) + btn_save_edits: ذخیره ویرایش ها + btn_cancel: لغو + tags: + title: برچسب ها + sort_buttons: + popular: محبوب + name: نام + newest: جدیدترین + button_follow: دنبال کردن + button_following: دنبال می کنید + tag_label: سوالات + search_placeholder: فیلتر بر اساس اسم برچسب + no_desc: برچسب هیچ توضیحی ندارد. + more: بیشتر + wiki: Wiki + ask: + title: Create Question + edit_title: سوال را ویرایش کنید + default_reason: سوال را ویرایش کنید + default_first_reason: Create question + similar_questions: سؤال های مشابه + form: + fields: + revision: + label: تجدید نظر + title: + label: عنوان + placeholder: What's your topic? Be specific. + msg: + empty: عنوان نمی تواند خالی باشد. + range: عنوان میتواند تا ۳۰ حرف باشد + body: + label: بدنه + msg: + empty: بدنه نمی تواند خالی باشد. + tags: + label: برچسب ها + msg: + empty: برچسب ها نمی تواند خالی باشد. + answer: + label: پاسخ + msg: + empty: جواب نمی تواند خالی باشد. + edit_summary: + label: ویرایش خلاصه + placeholder: >- + بطور خلاصه تغییرات را توضیح دهید (اصلاح املایی، اصلاح دستورزبان،‌ بهبود فرمت دهی) + btn_post_question: سوال خود را پست کنید + btn_save_edits: ذخیره ویرایش ها + answer_question: جواب دادن به سوال خودتان + post_question&answer: سوال خود را پست و جواب دهید + tag_selector: + add_btn: اضافه کردن برچسب + create_btn: ایجاد یک برچسب جدید + search_tag: جست‌وجوی برچسب‌ + hint: "Describe what your content is about, at least one tag is required." + no_result: هیچ تگی مطابقت ندارد + tag_required_text: تگ نیاز هست (حداقل یک مورد) + header: + nav: + question: سوالات + tag: تگ‌ها + user: کاربران + badges: Badges + profile: پروفایل + setting: تنظیمات + logout: خروج + admin: ادمین + review: بازبینی + bookmark: نشانک ها + moderation: مدیریت + search: + placeholder: جستجو + footer: + build_on: >- + پشتیبانی شده توسط <1> Apache Answer - نرم‌افزار منبع باز که باهمستان های پرسش و پاسخ را تقویت می کند.
ساخته شده با عشق © {{cc}}. + upload_img: + name: تغییر + loading: درحال بارگذاری... + pic_auth_code: + title: کپچا + placeholder: متن بالا را تاپ کنید + msg: + empty: کپچا نمی تواند خالی باشد. + inactive: + first: >- + شما تقریباً آماده شده اید! ما یک ایمیل فعال سازی به {{mail}} ارسال کردیم. لطفا دستورالعمل های ایمیل را برای فعال کردن حساب خود دنبال کنید. + info: "اگر ایمیل ارسالی را دریافت نکردید، قسمت spam خود را چک کنید." + another: >- + ایمیل فعال‌سازی دیگری را به آدرس {{mail}} برای شما ارسال کردیم. ممکن است چند دقیقه طول بکشد تا دستتان برسد. پوشه هرزنامه خود را حتما چک کنید. + btn_name: ارسال مجدد کد فعالسازی + change_btn_name: تغییر ایمیل + msg: + empty: نمی‌تواند خالی باشد. + resend_email: + url_label: آیا مطمئن هستید که می خواهید ایمیل فعال سازی را دوباره ارسال کنید؟ + url_text: همچنین می توانید لینک فعال سازی بالا را در اختیار کاربر قرار دهید. + login: + login_to_continue: برای ادامه وارد حساب کاربری شوید + info_sign: حساب کاربری ندارید؟ ثبت نام<1> + info_login: از قبل حساب کاربری دارید؟ <1>وارد شوید + agreements: با ثبت نام، با <1>خط مشی رازداری و <3>شرایط خدمات موافقت می کنید. + forgot_pass: رمزعبور را فراموش کردید? + name: + label: نام + msg: + empty: نام نمی‌تواند خالی باشد. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: ایمیل + msg: + empty: ایمیل نمی تواند خالی باشد. + password: + label: رمز عبور + msg: + empty: رمز عبور نمی تواند خالی باشد. + different: پسوردهای وارد شده در هر دو طرف متناقض هستند + account_forgot: + page_title: رمزعبور را فراموش کردید + btn_name: ارسال ایمیل بازیابی + send_success: >- + اگر یک حساب با {{mail}} مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید. + email: + label: ایمیل + msg: + empty: ایمیل نمی تواند خالی باشد. + change_email: + btn_cancel: لغو + btn_update: نشانی ایمیل را به روز کنید + send_success: >- + اگر یک حساب با {{mail}} مطابقت داشته باشد، باید به زودی ایمیلی حاوی دستورالعمل‌هایی درباره نحوه بازنشانی رمز عبور خود دریافت کنید. + email: + label: ایمیل جدید + msg: + empty: ایمیل نمی تواند خالی باشد. + oauth: + connect: ارتباط با {{ auth_name }} + remove: حذف {{ auth_name }} + oauth_bind_email: + subtitle: یک ایمیل بازیابی به حساب خود اضافه کنید. + btn_update: نشانی ایمیل را به روز کنید + email: + label: ایمیل + msg: + empty: ایمیل نمی تواند خالی باشد. + modal_title: ایمیل تکراری. + modal_content: این ایمیل قبلا ثبت نام کرده است. آیا مطمئن هستید که می خواهید به حساب ثبت نام کرده متصل شوید? + modal_cancel: تغییر ایمیل + modal_confirm: به حساب کاربری ثبت نام کرده متصل شوید + password_reset: + page_title: بازیابی کلمه عبور + btn_name: رمز عبورم را بازنشانی کن + reset_success: >- + شما با موفقیت رمز عبور خود را تغییر دادید، به صفحه ورود هدایت می شوید. + link_invalid: >- + متاسفم،‌ لینک بازنشانی رمز عبور دیگر اعتبار ندارد. شاید رمز عبور شما قبلا تغییر کرده است? + to_login: ادامه بدهید تا به صفحه ورود برسید + password: + label: رمز عبور + msg: + empty: رمز عبور نمی تواند خالی باشد. + length: تعداد حروف باید بین ۸ تا ۳۲ باشد + different: پسوردهای وارد شده در هر دو طرف متناقض هستند + password_confirm: + label: تأیید رمز عبور جديد + settings: + page_title: تنظیمات + goto_modify: برای تغییر بروید + nav: + profile: پروفایل + notification: اعلانات + account: حساب کاربری + interface: رابط کاربری + profile: + heading: پروفایل + btn_name: ذخیره + display_name: + label: نام + msg: نام نمی تواند خالی باشد. + msg_range: Display name must be 2-30 characters in length. + username: + label: نام‌کاربری + caption: دیگران میتوانند به شما به بصورت "@username" اشاره کنند. + msg: نام کاربری نمی تواند خالی باشد. + msg_range: Username must be 2-30 characters in length. + character: 'باید از حروف "a-z", "0-9", " - . _" استفاده شود' + avatar: + label: عکس پروفایل + gravatar: Gravatar + gravatar_text: می توانید تصویر را تغییر دهید + custom: سفارشی + custom_text: شما میتوانید عکس خود را بازگذاری کنید. + default: سیستم + msg: لطفا یک آواتار آپلود کنید + bio: + label: درباره من + website: + label: وب سایت + placeholder: "https://example.com" + msg: فرمت نادرست وب سایت + location: + label: موقعیت + placeholder: "شهر، کشور" + notification: + heading: اعلان های ایمیلی + turn_on: روشن کردن + inbox: + label: اعلانات ایمیل + description: پاسخ به سوالات خود،‌ نظرات،‌ دعوت ها،‌و بیشتر. + all_new_question: + label: تمامی سوالات جدید + description: درمورد تمامی سوالات جدید با خبر شوید. تا ۵۰ سوال در هفته. + all_new_question_for_following_tags: + label: تمامی سوالات جدید برای این تگ ها + description: درمورد تمامی سوالات جدید در مورد این تگ ها باخبر شوید. + account: + heading: حساب کاربری + change_email_btn: تغییر ایمیل + change_pass_btn: تغییر رمز عبور + change_email_info: >- + ما یک ایمیل به آن آدرس ارسال کردیم. لطفا مراحل تایید را طی کنید. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: رمز عبور فعلی + msg: رمز عبور نمی تواند خالی باشد. + password_title: رمز عبور + current_pass: + label: رمز عبور فعلی + msg: + empty: رمز عبور نمی تواند خالی باشد. + length: تعداد حروف باید بین ۸ تا ۳۲ باشد. + different: دو رمز عبور وارد شده همخوانی ندارند. + new_pass: + label: رمز عبور جدید + pass_confirm: + label: تأیید رمز عبور جديد + interface: + heading: رابط کاربری + lang: + label: زبان رابط کاربری + text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید. + my_logins: + title: ورود های من + label: با استفاده از این حساب ها وارد این سایت شوید یا ثبت نام کنید. + modal_title: حذف ورود + modal_content: آیا مطمئن هستید که می خواهید این ورود را از حساب خود حذف کنید؟ + modal_confirm_btn: حذف + remove_success: با موفقیت حذف شد + toast: + update: بروز رسانی موفق بود + update_password: رمز عبور با موفقیت تغییر کرد. + flag_success: ممنون بابت اطلاع دادن. + forbidden_operate_self: عملیات غیر مجاز + review: بازبینی شما پس از بررسی نشان داده خواهد شد. + sent_success: با موفقيت ارسال شد + related_question: + title: Related + answers: جواب ها + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: مردم پرسیدند + desc: افرادی را دعوت کنید که فکر می کنید ممکن است پاسخ را بدانند. + invite: دعوت به پاسخ + add: افزودن افراد + search: جستجوی افراد + question_detail: + action: عملیات + Asked: پرسیده شده + asked: پرسیده شده + update: تغییر یافته + edit: ویرایش شده + commented: commented + Views: مشاهده شده + Follow: دنبال کردن + Following: دنبال می کنید + follow_tip: برای دریافت اعلان ها این سوال را دنبال کنید + answered: جواب داده + closed_in: بسته شده د + show_exist: نمایش سوال موجود. + useful: مفید + question_useful: مفید و واضح است + question_un_useful: نامشخص یا مفید نیست + question_bookmark: این سوال را نشانه گذاری کنید + answer_useful: مفید است + answer_un_useful: مفید نیست + answers: + title: پاسخ ها + score: امتیاز + newest: جدیدترین + oldest: Oldest + btn_accept: پذیرفتن + btn_accepted: پذیرفته شده + write_answer: + title: پاسخ شما + edit_answer: پاسخ فعلی من را ویرایش کنید + btn_name: پاسخ خود را ارسال کنید + add_another_answer: پاسخ دیگری اضافه کنید + confirm_title: به پاسخ دادن ادامه دهید + continue: ادامه دهید + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: جواب نمی تواند خالی باشد. + characters: متن باید حداقل ۶ حرف داشته باشد. + tips: + header_1: از جواب شما متشکریم + li1_1: لطفا مطمئن شوید که جواب دهید سوال را. جزئیات بیشتری ارائه دهید و تحقیقات خود را به اشتراک بگذارید. + li1_2: از هر اظهاراتی که می کنید با ارجاعات یا تجربه شخصی پشتیبان بگیرید. + header_2: اما دوری کنید ... + li2_1: درخواست کمک، به دنبال شفاف سازی، یا پاسخ به پاسخ های دیگر. + reopen: + confirm_btn: بازگشایی مجدد + title: بازگشایی مجدد این پست + content: آیا مطمئن هستید که می‌خواهید بازگشایی مجدد انجام دهید? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: پست را پین کن + content: آیا مطمئن هستید میخواهید پست بصورت عمومی پین شود؟ پست در بالای تمامی پست ها نشان داده خواهد شد. + confirm_btn: پین کردن + delete: + title: حذف این پست + question: >- + ما حذف سوالات با جواب را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.

حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید? + answer_accepted: >- + ما حذف جواب تایید شده را پیشنهاد نمی کنیم، زیرا انجام این کار خوانندگان آینده را از این دانش محروم می کند.

حذف مکرر سؤالات پاسخ داده شده می تواند منجر به مسدود شدن حساب شما از سؤال شود. آیا مطمئن هستید که می خواهید حذف کنید? + other: مطمئنید که میخواهید حذف شود? + tip_answer_deleted: جواب شما حذف شده است + undelete_title: حذف این پست + undelete_desc: آیا مطمئن به بازگردانی هستید؟ + btns: + confirm: تایید + cancel: لغو + edit: ویرایش + save: ذخیره + delete: حذف + undelete: بازگردانی + list: List + unlist: Unlist + unlisted: Unlisted + login: ورود + signup: عضويت + logout: خروج + verify: تایید + create: Create + approve: تایید + reject: رد کردن + skip: بعدی + discard_draft: دور انداختن پیش‌نویس‌ + pinned: پین شد + all: همه + question: سوال + answer: پاسخ + comment: نظر + refresh: بارگذاری مجدد + resend: ارسال مجدد + deactivate: غیرفعال کردن + active: فعال + suspend: تعلیق + unsuspend: لغو تعلیق + close: بستن + reopen: بازگشایی + ok: تأیید + light: روشن + dark: تیره + system_setting: تنظیمات سامانه + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: نتایج جستجو + keywords: کلیدواژه ها + options: گزینه‌ها + follow: دنبال کردن + following: دنبال میکنید + counts: "{{count}} نتیجه" + counts_loading: "... Results" + more: بیشتر + sort_btns: + relevance: مرتبط + newest: جدیدترین + active: فعال + score: امتیاز + more: بیشتر + tips: + title: گزینه های پیشرفته جستجو + tag: "<1>[tag] search with a tag" + user: "<1>user:username جستجو براساس نویسنده" + answer: "<1>answers:0 سوال بی جواب" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: چیزی پیدا نکردیم
کلمات کلیدی متفاوت یا کمتر خاص را امتحان کنید. + share: + name: اشتراک‌گذاری + copy: کپی کردن لینک + via: اشتراک گذاری پست با... + copied: کپی انجام شد + facebook: اشتراک گذاری در فیس بوک + twitter: Share to X + cannot_vote_for_self: شما نمی توانید به پست خودتان رای دهید. + modal_confirm: + title: خطا... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: اکانت جدید شما تایید شده است، به صفحه خانه منتقل خواهید شد. + link: ادامه بدهید تا به صفحه خانه برسید + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: ایمیل شما به‌روز شده است. + confirm_new_email_invalid: >- + متاسفیم، لینک تایید دیگر مجاز نیست. شاید حساب شما از قبل فعال شده است? + unsubscribe: + page_title: قطع عضویت + success_title: لغو عضویت موفق (Automatic Translation) + success_desc: شما با موفقیت از این لیست مشترک حذف شده اید و دیگر ایمیلی از ما دریافت نخواهید کرد. + link: تغییر تنظیمات + question: + following_tags: تگهای مورد نظر + edit: ویرایش + save: ذخیره + follow_tag_tip: برچسب ها را دنبال کنید تا لیست سوالات خود را تنظیم کنید. + hot_questions: سوالات داغ + all_questions: تمام سوالات + x_questions: "{{ count }} سوال" + x_answers: "{{ count }} جواب" + x_posts: "{{ count }} Posts" + questions: سوالات + answers: پاسخ ها + newest: جدیدترین + active: فعال + hot: Hot + frequent: Frequent + recommend: Recommend + score: امتیاز + unanswered: بدون پاسخ + modified: تغییر یافته + answered: جواب داده + asked: پرسیده شده + closed: بسته + follow_a_tag: یک برچسب را دنبال کنید + more: بیشتر + personal: + overview: خلاصه + answers: پاسخ ها + answer: پاسخ + questions: سوالات + question: سوال + bookmarks: نشان ها + reputation: محبوبیت + comments: نظرات + votes: آراء + badges: Badges + newest: جدیدترین + score: امتیاز + edit_profile: ویرایش پروفایل + visited_x_days: "Visited {{ count }} days" + viewed: مشاهده شده + joined: عضو شد + comma: "," + last_login: مشاهده شده + about_me: درباره من + about_me_empty: "// Hello, World !" + top_answers: پاسخ های برتر + top_questions: سوالات برتر + stats: آمار + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: پذیرفته شده + answered: جواب داده + asked: پرسیده شده + downvoted: رأی منفی + mod_short: MOD + mod_long: Moderators + x_reputation: محبوبیت + x_votes: آرای دریافت شد + x_answers: جواب ها + x_questions: سوالات + recent_badges: Recent Badges + install: + title: Installation + next: بعدی + done: انجام شده + config_yaml_error: نمیتوان فایل config.yaml را ایجاد کرد. + lang: + label: لطفا زبان خود را انتخاب کنید + db_type: + label: موتور پایگاه‌داده + db_username: + label: نام‌کاربری + placeholder: روت + msg: نام کاربری نمی تواند خالی باشد. + db_password: + label: رمز عبور + placeholder: روت + msg: رمز عبور نمی تواند خالی باشد. + db_host: + label: میزبان پایگاه داده + placeholder: "db:3306" + msg: پایگاه داده میزبان نمیتواند خالی باشد. + db_name: + label: نام پایگاه‌داده + placeholder: پاسخ + msg: نام پایگاه داده میزبان نمیتواند خالی باشد. + db_file: + label: فایل پایگاه داده + placeholder: /data/answer.db + msg: فایل پایگاه داده نمیتواند خالی باشد. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Config.yaml را بسازید + label: فایل config.yaml ساخته شد. + desc: >- + شما بصورت دستی میتوانید فایل <1>config.yaml را در پوشه <1>/var/wwww/xxx/ ایجاد و متن را در داخل آن جایگذاری کنید. + info: بعد از اتمام،‌ بر روی "بعدی" کلیک کنید. + site_information: اطلاعات سایت + admin_account: حساب ادمین + site_name: + label: نام سایت + msg: نام سایت نمی تواند خالی باشد. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: آدرس سایت + text: آدرس سایت شما + msg: + empty: آدرس سایت نمی تواند خالی باشد. + incorrect: فرمت آدرس سایت نادرست است. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: ایمیل ارتباطی + text: آدرس ایمیل مخاطب کلیدی پاسخگو برای این سایت. + msg: + empty: ایمیل مخاطب نمی تواند خالی باشد. + incorrect: فرمت ایمیل مخاطب نادرست است. + login_required: + label: خصوصی + switch: ورود الزامی است + text: تنها افرادی که وارد سایت شده اند میتوانند به این انجمن دسترسی پیدا کنند. + admin_name: + label: نام + msg: نام نمی‌تواند خالی باشد. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: رمز عبور + text: >- + شما برای ورود نیازمند این پسورد خواهید بود. لطفا در محل امنی ذخیره کنید. + msg: رمز عبور نمی تواند خالی باشد. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: ایمیل + text: شما به این ایمیل برای ورود نیاز خواهید داشت. + msg: + empty: ایمیل نمی تواند خالی باشد. + incorrect: فرمت ایمیل نادرست است. + ready_title: Your site is ready + ready_desc: >- + اگر به تغییر بیشتر تنظیمات نیاز دارید،‌به <1> قسمت ادمین مراجعه کنید،‌میتوانید این گزینه را در منو سایت مشاهده کنید. + good_luck: "خوش بگذره و موفق باشید!" + warn_title: هشدار + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: قبلاً نصب شده است + installed_desc: >- + شما پیش از‌این وردپرس را برپا نموده‌اید. برای راه‌اندازی دوباره ابتدا جدول‌های کهنه در پایگاه‌داده را پاک نمایید. + db_failed: اتصال به دیتابیس موفقیت آمیز نبود + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: مشاهده + votes: آراء + answers: جواب ها + accepted: پذیرفته شده + page_error: + http_error: HTTP Error {{ code }} + desc_403: شما مجوز دسترسی به این صفحه را ندارید. + desc_404: متاسفانه این صفحه وجود ندارد. + desc_50X: The server encountered an error and could not complete your request. + back_home: بازگشت به صفحه اصلی + page_maintenance: + desc: "ما درحال تعمیر هستیم، به زودی برمی گردیم." + nav_menus: + dashboard: داشبرد + contents: محتوا + questions: سوالات + answers: پاسخ ها + users: کاربران + badges: Badges + flags: پرچم + settings: تنظیمات + general: عمومی + interface: رابط کاربری + smtp: SMTP + branding: نام تجاری + legal: قانونی + write: نوشتن + tos: قوانین + privacy: حریم خصوصی + seo: سئو + customize: شخصی‌سازی + themes: پوسته ها + login: ورود + privileges: مجوزها + plugins: افزونه‌ها + installed_plugins: پلاگین های نصب شده + apperance: Appearance + website_welcome: به {{site_name}} خوش آمدید + user_center: + login: ورود + qrcode_login_tip: لطفاً از {{ agentName }} برای اسکن کد QR و ورود به سیستم استفاده کنید. + login_failed_email_tip: ورود ناموفق بود، لطفاً قبل از امتحان مجدد به این برنامه اجازه دهید به اطلاعات ایمیل شما دسترسی داشته باشد. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: ادمین + dashboard: + title: داشبرد + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "سوالات:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "جواب ها:" + comments: "نظرات:" + votes: "آرا:" + users: "Users:" + flags: "علامت‌ها:" + reviews: "Reviews:" + site_health: Site health + version: "نسخه:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: در دسترس عموم + smtp: "SMTP:" + timezone: "منطقه زمانی:" + system_info: اطلاعات سامانه + go_version: "نسخه Go:" + database: "پایگاه داده:" + database_size: "اندازه پایگاه داده :" + storage_used: "فضای استفاده شده:" + uptime: "پایداری:" + links: Links + plugins: افزونه‌ها + github: GitHub + blog: بلاگ + contact: تماس + forum: Forum + documents: اسناد + feedback: ﺑﺎﺯﺧﻮﺭﺩ + support: پشتیبانی + review: بازبینی + config: کانفینگ + update_to: آپدیت کردن به + latest: آخرین + check_failed: بررسی ناموفق بود + "yes": "بله" + "no": "نه" + not_allowed: غیر مجاز + allowed: مجاز + enabled: فعال + disabled: غیرفعال + writable: قابل نوشتن + not_writable: غیرقابل کُپی + flags: + title: علامت‌ها + pending: در حالت انتظار + completed: تکمیل شده + flagged: علامت گذاری شده + flagged_type: پرچم گذاری شده {{ type }} + created: ایجاد شده + action: عمل + review: بازبینی + user_role_modal: + title: تغییر نقش کاربر به ... + btn_cancel: لغو + btn_submit: ثبت + new_password_modal: + title: تعیین رمزعبور جدید + form: + fields: + password: + label: رمز عبور + text: کاربر از سیستم خارج می شود و باید دوباره وارد سیستم شود. + msg: رمز عبور باید 8 تا 32 کاراکتر باشد. + btn_cancel: لغو + btn_submit: ثبت + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: افزودن کاربر جدید + form: + fields: + users: + label: اضافه کردن انبوه کاربر + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: '"نام، ایمیل، رمز عبور" را با کاما جدا کنید. یک کاربر در هر خط.' + msg: "لطفا ایمیل کاربر را در هر خط یکی وارد کنید." + display_name: + label: نمایش نام + msg: Display name must be 2-30 characters in length. + email: + label: ایمیل + msg: ایمیل معتبر نمی‌باشد + password: + label: رمز عبور + msg: رمز عبور باید 8 تا 32 کاراکتر باشد. + btn_cancel: لغو + btn_submit: ثبت + users: + title: کاربرها + name: نام + email: ایمیل + reputation: محبوبیت + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: وضعیت + role: نقش + action: عمل + change: تغییر + all: همه + staff: کارکنان + more: بیشتر + inactive: غیرفعال + suspended: تعلیق شده + deleted: حذف شده + normal: عادي + Moderator: مدير + Admin: ادمین + User: کاربر + filter: + placeholder: "فیلتر براساس، user:id" + set_new_password: تعیین رمزعبور جدید + edit_profile: Edit profile + change_status: تغییر وضعیت + change_role: تغییر نقش + show_logs: نمایش ورود ها + add_user: افزودن کاربر + deactivate_user: + title: غیر فعال کردن کاربر + content: یک کاربر غیرفعال باید ایمیل خود را دوباره تایید کند. + delete_user: + title: این کاربر حذف شود + content: آیا مطمئن هستید که میخواهید این کابر را حذف نمایید؟ این اقدام دائمی است! + remove: محتوای آنها را حذف کنید + label: تمام سوالات، پاسخ ها، نظرات و غیره را حذف کن. + text: اگر می‌خواهید فقط حساب کاربر را حذف کنید، این را بررسی نکنید. + suspend_user: + title: تعلیق این کاربر + content: کاربر تعلیق شده نمی‌تواند وارد شود. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: سوالات + unlisted: Unlisted + post: پست + votes: آراء + answers: پاسخ ها + created: ایجاد شده + status: وضعیت + action: عمل + change: تغییر + pending: Pending + filter: + placeholder: "فیلتر براساس، user:id" + answers: + page_title: پاسخ ها + post: پست + votes: آراء + created: ایجاد شده + status: وضعیت + action: اقدام + change: تغییر + filter: + placeholder: "فیلتر براساس، user:id" + general: + page_title: عمومی + name: + label: نام سایت + msg: نام سایت نمی تواند خالی باشد. + text: "نام این سایت همانطور که در تگ عنوان استفاده شده است." + site_url: + label: آدرس سایت + msg: آدرس سایت نمی تواند خالی باشد. + validate: لطفا یک url معتبر وارد کنید. + text: آدرس سایت شما. + short_desc: + label: نام این سایت مورد استفاده قرار گرفته است + msg: توضیحات کوتاه سایت نمی تواند خالی باشد. + text: "شرح کوتاه، همانطور که در تگ عنوان در صفحه اصلی استفاده شده است." + desc: + label: توضیحات سایت + msg: توضیحات کوتاه سایت نمی تواند خالی باشد. + text: "همانطور که در تگ توضیحات متا استفاده شده است، این سایت را در یک جمله توصیف کنید." + contact_email: + label: ایمیل ارتباطی + msg: ایمیل مخاطب نمی تواند خالی باشد. + validate: ایمیل تماس معتبر نیست. + text: آدرس ایمیل مخاطب کلیدی مسئول این سایت. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: رابط کاربری + language: + label: زبان رابط کاربری + msg: زبان رابط نمی تواند خالی باشد. + text: زبان رابط کاربری. زمانی تغییر می کند که صفحه را دوباره بارگذاری کنید. + time_zone: + label: منطقه زمانی + msg: منطقه زمانی نمی تواند خالی باشد. + text: شهری را در همان منطقه زمانی خود انتخاب کنید. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: از ایمیل + msg: ایمیل نباید خالی باشد. + text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند. + from_name: + label: نام فرستنده + msg: ایمیل نباید خالی باشد. + text: آدرس ایمیلی که ایمیل ها از آن ارسال می شوند. + smtp_host: + label: ميزبان SMTP + msg: میزبان SMTP نمی تواند خالی باشد. + text: سرور ایمیل شما. + encryption: + label: رمزگذاری + msg: کلید رمزگذاری نمی تواند خالی باشد. + text: برای اکثر سرورها SSL گزینه پیشنهادی است. + ssl: SSL + tls: TLS + none: هیچ‌کدام + smtp_port: + label: پورت SMTP + msg: پورت SMTP باید شماره 1 ~ 65535 باشد. + text: پورت سرور ایمیل شما. + smtp_username: + label: نام کاربری SMTP + msg: نام کاربری SMTP نمی تواند خالی باشد. + smtp_password: + label: رمزعبور SMTP + msg: رمز عبور مدیر نمی‌تواند خالی باشد. + test_email_recipient: + label: گیرندگان ایمیل را آزمایش کنید + text: آدرس ایمیلی را ارائه دهید که ارسال های آزمایشی را دریافت می کند. + msg: گیرندگان ایمیل آزمایشی نامعتبر است + smtp_authentication: + label: فعال کردن احراز هویت + title: تأیید هویت SMTP + msg: احراز هویت SMTP نمی تواند خالی باشد. + "yes": "بله" + "no": "نه" + branding: + page_title: نام تجاری + logo: + label: لوگو + msg: کد نمی تواند خالی باشد. + text: تصویر لوگو در سمت چپ بالای سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 و نسبت تصویر بیشتر از 3:1 استفاده کنید. اگر خالی بماند، متن عنوان سایت نشان داده می شود. + mobile_logo: + label: لوگوی موبایل + text: لوگوی مورد استفاده در نسخه موبایلی سایت شما. از یک تصویر مستطیلی عریض با ارتفاع 56 استفاده کنید. اگر خالی بماند، تصویر از تنظیمات "لوگو" استفاده خواهد شد. + square_icon: + label: نماد مربعی + msg: نماد مربعی نمی تواند خالی باشد. + text: تصویر به عنوان پایه نمادهای متادیتا استفاده می شود. در حالت ایده آل باید بزرگتر از 512x512 باشد. + favicon: + label: نمادک + text: فاویکون برای سایت شما. برای اینکه روی CDN به درستی کار کند باید یک png باشد. به 32x32 تغییر اندازه خواهد شد. اگر خالی بماند، از "نماد مربع" استفاده می شود. + legal: + page_title: قانونی + terms_of_service: + label: شرایط خدمات + text: "می توانید محتوای شرایط خدمات را در اینجا اضافه کنید. اگر قبلاً سندی دارید که در جای دیگری میزبانی شده است، URL کامل را در اینجا ارائه دهید." + privacy_policy: + label: حریم خصوصی + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: نوشتن + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: بهینه‌سازی عملیات موتورهای جستجو + permalink: + label: پیوند ثابت + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: دامنه های مجاز ایمیل + text: دامنه های ایمیلی که کاربران باید با آنها حساب ثبت کنند. یک دامنه در هر خط. وقتی خالی است نادیده گرفته می شود. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: وضعیت + action: اقدام + deactivate: Deactivate + activate: فعال سازی + settings: تنظیمات + settings_users: + title: کاربران + avatar: + label: آواتار پیش فرض + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar Base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: پست فهرست نشده + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/fi_FI.yaml b/i18n/fi_FI.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/fi_FI.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- +

+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/fr_FR.yaml b/i18n/fr_FR.yaml new file mode 100644 index 000000000..0be195741 --- /dev/null +++ b/i18n/fr_FR.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Succès. + unknown: + other: Erreur inconnue. + request_format_error: + other: Format de fichier incorrect. + unauthorized_error: + other: Non autorisé. + database_error: + other: Erreur du serveur de données. + forbidden_error: + other: Interdit. + duplicate_request_error: + other: Soumission en double. + action: + report: + other: Signaler + edit: + other: Éditer + delete: + other: Supprimer + close: + other: Fermer + reopen: + other: Rouvrir + forbidden_error: + other: Interdit. + pin: + other: Épingler + hide: + other: Délister + unpin: + other: Désépingler + show: + other: Liste + invite_someone_to_answer: + other: Modifier + undelete: + other: Annuler la suppression + merge: + other: Fusionner + role: + name: + user: + other: Utilisateur + admin: + other: Administrateur + moderator: + other: Modérateur + description: + user: + other: Par défaut, sans accès spécial. + admin: + other: Possède tous les droits pour accéder au site. + moderator: + other: Possède les accès à tous les messages sauf aux paramètres d'administration. + privilege: + level_1: + description: + other: Niveau 1 (moins de réputation requise pour une équipe privée, un groupe) + level_2: + description: + other: Niveau 2 (faible réputation requise pour la communauté des startups) + level_3: + description: + other: Niveau 3 (haute réputation requise pour une communauté mature) + level_custom: + description: + other: Niveau personnalisé + rank_question_add_label: + other: Poser une question + rank_answer_add_label: + other: Écrire une réponse + rank_comment_add_label: + other: Ajouter un commentaire + rank_report_add_label: + other: Signaler + rank_comment_vote_up_label: + other: Voter favorablement le commentaire + rank_link_url_limit_label: + other: Poster plus de 2 liens à la fois + rank_question_vote_up_label: + other: Voter favorablement la question + rank_answer_vote_up_label: + other: Voter favorablement la réponse + rank_question_vote_down_label: + other: Voter contre la question + rank_answer_vote_down_label: + other: Voter contre la réponse + rank_invite_someone_to_answer_label: + other: Inviter quelqu'un à répondre + rank_tag_add_label: + other: Créer une nouvelle étiquette + rank_tag_edit_label: + other: Modifier la description de la balise (à réviser) + rank_question_edit_label: + other: Modifier la question des autres (à revoir) + rank_answer_edit_label: + other: Modifier la réponse d'un autre (à revoir) + rank_question_edit_without_review_label: + other: Modifier la question d'un autre utilisateur sans révision + rank_answer_edit_without_review_label: + other: Modifier la réponse d'un autre utilisateur sans révision + rank_question_audit_label: + other: Vérifier la question + rank_answer_audit_label: + other: Revoir les modifications de la réponse + rank_tag_audit_label: + other: Évaluer les modifications des tags + rank_tag_edit_without_review_label: + other: Modifier la description du tag sans révision + rank_tag_synonym_label: + other: Gérer les tags synonyme + email: + other: Email + e_mail: + other: Email + password: + other: Mot de passe + pass: + other: Mot de passe + old_pass: + other: Mot de passe actuel + original_text: + other: Ce post + email_or_password_wrong_error: + other: L'email et le mot de passe ne correspondent pas. + error: + common: + invalid_url: + other: URL invalide. + status_invalid: + other: Statut invalide. + password: + space_invalid: + other: Le mot de passe ne doit pas comporter d'espaces. + admin: + cannot_update_their_password: + other: Vous ne pouvez pas modifier votre mot de passe. + cannot_edit_their_profile: + other: Vous ne pouvez pas modifier votre profil. + cannot_modify_self_status: + other: Vous ne pouvez pas modifier votre statut. + email_or_password_wrong: + other: L'email et le mot de passe ne correspondent pas. + answer: + not_found: + other: Réponse introuvable. + cannot_deleted: + other: Pas de permission pour supprimer. + cannot_update: + other: Pas de permission pour mettre à jour. + question_closed_cannot_add: + other: Les questions sont fermées et ne peuvent pas être ajoutées. + content_cannot_empty: + other: La réponse ne peut être vide. + comment: + edit_without_permission: + other: Les commentaires ne sont pas autorisés à être modifiés. + not_found: + other: Commentaire non trouvé. + cannot_edit_after_deadline: + other: Le commentaire a été posté il y a trop longtemps pour être modifié. + content_cannot_empty: + other: Le commentaire ne peut être vide. + email: + duplicate: + other: L'adresse e-mail existe déjà. + need_to_be_verified: + other: L'adresse e-mail doit être vérifiée. + verify_url_expired: + other: L'URL de vérification de l'email a expiré, veuillez renvoyer l'email. + illegal_email_domain_error: + other: L'e-mail n'est pas autorisé à partir de ce domaine de messagerie. Veuillez en utiliser un autre. + lang: + not_found: + other: Fichier de langue non trouvé. + object: + captcha_verification_failed: + other: Le Captcha est incorrect. + disallow_follow: + other: Vous n’êtes pas autorisé à suivre. + disallow_vote: + other: Vous n’êtes pas autorisé à voter. + disallow_vote_your_self: + other: Vous ne pouvez pas voter pour votre propre message. + not_found: + other: Objet non trouvé. + verification_failed: + other: La vérification a échoué. + email_or_password_incorrect: + other: L'e-mail et le mot de passe ne correspondent pas. + old_password_verification_failed: + other: La vérification de l'ancien mot de passe a échoué + new_password_same_as_previous_setting: + other: Le nouveau mot de passe est le même que le précédent. + already_deleted: + other: Ce post a été supprimé. + meta: + object_not_found: + other: Méta objet introuvable + question: + already_deleted: + other: Ce message a été supprimé. + under_review: + other: Votre message est en attente de révision. Il sera visible une fois approuvé. + not_found: + other: Question non trouvée. + cannot_deleted: + other: Pas de permission pour supprimer. + cannot_close: + other: Pas de permission pour fermer. + cannot_update: + other: Pas de permission pour mettre à jour. + content_cannot_empty: + other: Le contenu ne peut pas être vide. + rank: + fail_to_meet_the_condition: + other: Le rang de réputation ne remplit pas la condition. + vote_fail_to_meet_the_condition: + other: Merci pour vos commentaires. Vous avez besoin d'au moins {{.Rank}} de réputation pour voter. + no_enough_rank_to_operate: + other: Vous avez besoin d'au moins {{.Rank}} de réputation pour faire cela. + report: + handle_failed: + other: La gestion du rapport a échoué. + not_found: + other: Rapport non trouvé. + tag: + already_exist: + other: Le tag existe déjà. + not_found: + other: Tag non trouvé. + recommend_tag_not_found: + other: Le tag Recommandé n'existe pas. + recommend_tag_enter: + other: Veuillez saisir au moins un tag. + not_contain_synonym_tags: + other: Ne dois pas contenir de tags synonymes. + cannot_update: + other: Pas de permission pour mettre à jour. + is_used_cannot_delete: + other: Vous ne pouvez pas supprimer un tag utilisé. + cannot_set_synonym_as_itself: + other: Vous ne pouvez pas définir le synonyme de la balise actuelle comme elle-même. + smtp: + config_from_name_cannot_be_email: + other: Le nom d'expéditeur ne peut pas être une adresse e-mail. + theme: + not_found: + other: Thème non trouvé. + revision: + review_underway: + other: Impossible d'éditer actuellement, il y a une version dans la file d'attente des revues. + no_permission: + other: Aucune autorisation de réviser. + user: + external_login_missing_user_id: + other: La plateforme tierce ne fournit pas un identifiant d'utilisateur unique, vous ne pouvez donc pas vous connecter, veuillez contacter l'administrateur du site. + external_login_unbinding_forbidden: + other: Veuillez définir un mot de passe de connexion pour votre compte avant de supprimer ce login. + email_or_password_wrong: + other: + other: L'email et le mot de passe ne correspondent pas. + not_found: + other: Utilisateur non trouvé. + suspended: + other: L'utilisateur a été suspendu. + username_invalid: + other: Le nom d'utilisateur est invalide. + username_duplicate: + other: Nom d'utilisateur déjà utilisé. + set_avatar: + other: La configuration de l'avatar a échoué. + cannot_update_your_role: + other: Vous ne pouvez pas modifier votre rôle. + not_allowed_registration: + other: Actuellement, le site n'est pas ouvert aux inscriptions. + not_allowed_login_via_password: + other: Actuellement le site n'est pas autorisé à se connecter par mot de passe. + access_denied: + other: Accès refusé + page_access_denied: + other: Vous n'avez pas accès à cette page. + add_bulk_users_format_error: + other: "Erreur format {{.Field}} près de '{{.Content}}' à la ligne {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Le nombre d'utilisateurs que vous ajoutez simultanément doit être compris entre 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: La lecture de la configuration a échoué + database: + connection_failed: + other: La connexion à la base de données a échoué + create_table_failed: + other: La création de la table a échoué + install: + create_config_failed: + other: Impossible de créer le fichier config.yaml. + upload: + unsupported_file_format: + other: Format de fichier non supporté. + site_info: + config_not_found: + other: Configuration du site introuvable. + badge: + object_not_found: + other: Objet badge introuvable + reason: + spam: + name: + other: Courrier indésirable + desc: + other: Ce message est une publicité ou un vandalisme. Il n'est pas utile ou pertinent pour le sujet actuel. + rude_or_abusive: + name: + other: grossier ou abusif + desc: + other: "Une personne raisonnable trouverait ce contenu inapproprié pour un discours respectueux." + a_duplicate: + name: + other: un doublon + desc: + other: Cette question a déjà été posée et a déjà une réponse. + placeholder: + other: Entrez le lien de la question existante + not_a_answer: + name: + other: n'est pas une réponse + desc: + other: "Cela a été posté comme une réponse, mais il n'essaie pas de répondre à la question. Il devrait s'agir d'un commentaire, d'une autre question, ou devrait être supprimé totalement." + no_longer_needed: + name: + other: ce n’est plus nécessaire + desc: + other: Ce commentaire est obsolète, conversationnel ou non pertinent pour ce post. + something: + name: + other: quelque chose d'autre + desc: + other: Ce message nécessite l'attention de l'équipe de modération pour une autre raison non listée ci-dessus. + placeholder: + other: Faites-nous savoir précisément ce qui vous préoccupe + community_specific: + name: + other: une raison spécifique à la communauté + desc: + other: Cette question ne répond pas à une directive de la communauté. + not_clarity: + name: + other: nécessite plus de détails ou de clarté + desc: + other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème. + looks_ok: + name: + other: semble bien + desc: + other: Ce poste est bon en tant que tel et n'est pas de mauvaise qualité. + needs_edit: + name: + other: a besoin d'être modifié, et je l'ai fait + desc: + other: Améliorez et corrigez vous-même les problèmes liés à ce message. + needs_close: + name: + other: a besoin de fermer + desc: + other: Une question fermée ne peut pas être répondue, mais peut-être quand même modifiée, votée et commentée. + needs_delete: + name: + other: a besoin d'être supprimé + desc: + other: Ce message sera supprimé. + question: + close: + duplicate: + name: + other: courrier indésirable + desc: + other: Cette question a déjà été posée auparavant et a déjà une réponse. + guideline: + name: + other: une raison spécifique à la communauté + desc: + other: Cette question ne répond pas à une directive de la communauté. + multiple: + name: + other: a besoin de détails ou de clarté + desc: + other: Cette question comprend actuellement plusieurs questions en une seule. Elle ne devrait se concentrer que sur un seul problème. + other: + name: + other: quelque chose d'autre + desc: + other: Ce message nécessite l'attention du personnel pour une autre raison non listée ci-dessus. + operation_type: + asked: + other: demandé + answered: + other: répondu + modified: + other: modifié + deleted_title: + other: Question supprimée + questions_title: + other: Questions + tag: + tags_title: + other: Étiquettes + no_description: + other: L'étiquette n'a pas de description. + notification: + action: + update_question: + other: question mise à jour + answer_the_question: + other: question répondue + update_answer: + other: réponse mise à jour + accept_answer: + other: réponse acceptée + comment_question: + other: a commenté la question + comment_answer: + other: a commenté la réponse + reply_to_you: + other: vous a répondu + mention_you: + other: vous a mentionné + your_question_is_closed: + other: Une réponse a été publiée pour votre question + your_question_was_deleted: + other: Une réponse a été publiée pour votre question + your_answer_was_deleted: + other: Votre réponse a bien été supprimée + your_comment_was_deleted: + other: Votre commentaire a été supprimé + up_voted_question: + other: question approuvée + down_voted_question: + other: question défavorisée + up_voted_answer: + other: voter favorablement la réponse + down_voted_answer: + other: réponse défavorisée + up_voted_comment: + other: commentaire approuvé + invited_you_to_answer: + other: vous invite à répondre + earned_badge: + other: Vous avez gagné le badge "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirmez votre nouvelle adresse e-mail" + body: + other: "Confirmez votre nouvelle adresse électronique pour {{.SiteName}} en cliquant sur le lien suivant :
\\n{{.ChangeEmailUrl}}

\\n\\nSi vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} a répondu à votre question" + body: + other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
{{.AnswerSummary}}

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} vous a invité à répondre" + body: + other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
Je pense que vous pourriez connaître la réponse.

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} a commenté votre message" + body: + other: "{{.QuestionTitle}}

\\n\\n{{.DisplayName}}:
\\n
{{.CommentSummary}}

\\nVoir sur {{.SiteName}}

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue.

\\n\\nDésabonner" + new_question: + title: + other: "[{{.SiteName}}] Nouvelle question : {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote : Il s'agit d'un e-mail automatique, merci de ne pas répondre à ce message, votre réponse ne pourra être considérée.

\n\nSe désabonner" + pass_reset: + title: + other: "[{{.SiteName }}] Réinitialisation du mot de passe" + body: + other: "Quelqu'un a demandé à réinitialiser votre mot de passe sur {{.SiteName}}.

\\n\\nSi ce n'était pas vous, vous pouvez ignorer cet e-mail en toute sécurité.

\\n\\nCliquez sur le lien suivant pour choisir un nouveau mot de passe :
\\n{{.PassResetUrl}}\\n

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." + register: + title: + other: "[{{.SiteName}}] Confirmez la création de votre compte" + body: + other: "Bienvenue sur {{.SiteName}}!

\\n\\nCliquez sur le lien suivant pour confirmer et activer votre nouveau compte :
\\n{{.RegisterUrl}}

\\n\\nSi le lien ci-dessus n'est pas cliquable, essayez de le copier et de le coller dans la barre d'adresse de votre navigateur web.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." + test: + title: + other: "[{{.SiteName}}] Email de test" + body: + other: "Ceci est un e-mail de test.

\\n\\n--
\\nNote : Ceci est un e-mail automatisé du système, merci de ne pas répondre à ce message car votre réponse ne sera pas vue." + action_activity_type: + upvote: + other: vote positif + upvoted: + other: voté pour + downvote: + other: voter contre + downvoted: + other: voté contre + accept: + other: accepter + accepted: + other: accepté + edit: + other: éditer + review: + queued_post: + other: Post en file d'attente + flagged_post: + other: Signaler post + suggested_post_edit: + other: Modifications suggérées + reaction: + tooltip: + other: "{{ .Names }} et {{ .Count }} de plus..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographe + desc: + other: Informations sur le profil. + certified: + name: + other: Certifié + desc: + other: Nous avons terminé notre nouveau tutoriel d'utilisation. + editor: + name: + other: Éditeur + desc: + other: Première modification du post. + first_flag: + name: + other: Premier drapeau + desc: + other: Premier a signalé un post. + first_upvote: + name: + other: Premier vote positif + desc: + other: Premier a signalé un post. + first_link: + name: + other: Premier lien + desc: + other: A ajouté un lien vers un autre message. + first_reaction: + name: + other: Première réaction + desc: + other: Première réaction au post. + first_share: + name: + other: Premier partage + desc: + other: Premier post partagé. + scholar: + name: + other: Érudit + desc: + other: A posé une question et accepté une réponse. + commentator: + name: + other: Commentateur + desc: + other: Laissez 5 commentaires. + new_user_of_the_month: + name: + other: Nouvel utilisateur du mois + desc: + other: Contributions en suspens au cours de leur premier mois. + read_guidelines: + name: + other: Lire les lignes de conduite + desc: + other: Lisez les [lignes directrices de la communauté]. + reader: + name: + other: Lecteur + desc: + other: Lisez toutes les réponses dans un sujet avec plus de 10 réponses. + welcome: + name: + other: Bienvenue + desc: + other: A reçu un vote positif. + nice_share: + name: + other: Bien partagé + desc: + other: A partagé un poste avec 25 visiteurs uniques. + good_share: + name: + other: Bon partage + desc: + other: A partagé un poste avec 300 visiteurs uniques. + great_share: + name: + other: Super Partage + desc: + other: A partagé un poste avec 1000 visiteurs uniques. + out_of_love: + name: + other: Amoureux + desc: + other: A donné 50 likes dans une journée. + higher_love: + name: + other: Amour plus grand + desc: + other: A donné 50 likes dans une journée 5 fois. + crazy_in_love: + name: + other: Fou d'amour + desc: + other: A recueilli 50 votes positifs par jour 20 fois. + promoter: + name: + other: Promoteur + desc: + other: Inviter un utilisateur. + campaigner: + name: + other: Propagandiste + desc: + other: A invité 3 utilisateurs de base. + champion: + name: + other: Champion + desc: + other: A invité 5 membres. + thank_you: + name: + other: Merci + desc: + other: A 20 postes votés et a donné 10 votes. + gives_back: + name: + other: Redonne + desc: + other: A 100 postes votés et a donné 100 votes. + empathetic: + name: + other: Empathique + desc: + other: A 500 postes votés et a donné 1000 votes. + enthusiast: + name: + other: Enthousiaste + desc: + other: Visite de 10 jours consécutifs. + aficionado: + name: + other: Aficionado + desc: + other: Visite de 100 jours consécutifs. + devotee: + name: + other: Devotee + desc: + other: Visite de 365 jours consécutifs. + anniversary: + name: + other: Anniversaire + desc: + other: Membre actif pour une année, affiché au moins une fois. + appreciated: + name: + other: Apprécié + desc: + other: A reçu 1 vote positif sur 20 posts. + respected: + name: + other: Respecté + desc: + other: A reçu 2 vote positif sur 100 posts. + admired: + name: + other: Admirée + desc: + other: A reçu 5 vote positif sur 300 messages. + solved: + name: + other: Résolu + desc: + other: Une réponse a été acceptée. + guidance_counsellor: + name: + other: Conseiller d'orientation + desc: + other: 10 réponses sont acceptées. + know_it_all: + name: + other: Tout-savoir + desc: + other: 50 réponses sont acceptées. + solution_institution: + name: + other: Institution de solution + desc: + other: 150 réponses sont acceptées. + nice_answer: + name: + other: Belle réponse + desc: + other: Réponse a obtenu un score de 10 ou plus. + good_answer: + name: + other: Bonne répone + desc: + other: Réponse a obtenu un score de 25 ou plus. + great_answer: + name: + other: Super Réponse + desc: + other: Réponse a obtenu un score de 50 ou plus. + nice_question: + name: + other: Belle Question + desc: + other: Question a obtenu un score de 10 ou plus. + good_question: + name: + other: Bonne Question + desc: + other: Question a obtenu un score de 25 ou plus. + great_question: + name: + other: Super Question + desc: + other: Question a obtenu un score de 50 ou plus. + popular_question: + name: + other: Question Populaire + desc: + other: Question avec 500 points de vue. + notable_question: + name: + other: Question notable + desc: + other: Question avec 1,000 points de vue. + famous_question: + name: + other: Question célèbre + desc: + other: Question avec 5000 points de vue. + popular_link: + name: + other: Lien populaire + desc: + other: A posté un lien externe avec 50 clics. + hot_link: + name: + other: Lien chaud + desc: + other: A posté un lien externe avec 300 clics. + famous_link: + name: + other: Célèbre lien + desc: + other: A posté un lien externe avec 100 clics. + default_badge_groups: + getting_started: + name: + other: Initialisation complète + community: + name: + other: Communauté + posting: + name: + other: Publication +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Comment mettre en forme + desc: >- + + pagination: + prev: Préc + next: Suivant + page_title: + question: Question + questions: Questions + tag: Étiquette + tags: Étiquettes + tag_wiki: tag wiki + create_tag: Créer un tag + edit_tag: Modifier l'étiquette + ask_a_question: Create Question + edit_question: Modifier la question + edit_answer: Modifier la réponse + search: Rechercher + posts_containing: Messages contenant + settings: Paramètres + notifications: Notifications + login: Se connecter + sign_up: S'inscrire + account_recovery: Récupération de compte + account_activation: Activation du compte + confirm_email: Confirmer l'email + account_suspended: Compte suspendu + admin: Admin + change_email: Modifier l'e-mail + install: Installation d'Answer + upgrade: Mise à jour d'Answer + maintenance: Maintenance du site + users: Utilisateurs + oauth_callback: Traitement + http_404: Erreur HTTP 404 + http_50X: Erreur HTTP 500 + http_403: Erreur HTTP 403 + logout: Se déconnecter + notifications: + title: Notifications + inbox: Boîte de réception + achievement: Accomplissements + new_alerts: Nouvelles notifications + all_read: Tout marquer comme lu + show_more: Afficher plus + someone: Quelqu'un + inbox_type: + all: Tous + posts: Publications + invites: Invitations + votes: Votes + answer: Réponse + question: Question + badge_award: Badge + suspended: + title: Votre compte a été suspendu + until_time: "Votre compte a été suspendu jusqu'au {{ time }}." + forever: Cet utilisateur a été suspendu pour toujours. + end: Vous ne respectez pas les directives de la communauté. + contact_us: Contactez-nous + editor: + blockquote: + text: Bloc de citation + bold: + text: Gras + chart: + text: Diagramme + flow_chart: Organigramme + sequence_diagram: Diagramme de séquence + class_diagram: Diagramme de classe + state_diagram: Diagramme d'état + entity_relationship_diagram: Diagramme entité-association + user_defined_diagram: Diagramme défini par l'utilisateur + gantt_chart: Diagramme de Gantt + pie_chart: Camembert + code: + text: Exemple de Code + add_code: Ajouter un exemple de code + form: + fields: + code: + label: Code + msg: + empty: Le code ne peut pas être vide. + language: + label: Langage + placeholder: Détection automatique + btn_cancel: Annuler + btn_confirm: Ajouter + formula: + text: Formule + options: + inline: Formule en ligne + block: Bloc de formule + heading: + text: Titre + options: + h1: Titre de niveau 1 + h2: Titre de niveau 2 + h3: Titre de niveau 3 + h4: Titre de niveau 4 + h5: Titre de niveau 5 + h6: Titre de niveau 6 + help: + text: Aide + hr: + text: Ligne horizontale + image: + text: Image + add_image: Ajouter une image + tab_image: Téléverser une image + form_image: + fields: + file: + label: Fichier image + btn: Sélectionner une image + msg: + empty: Le fichier ne doit pas être vide. + only_image: Seules les images sont autorisées. + max_size: La taille du fichier ne doit pas dépasser {{size}} Mo. + desc: + label: Description + tab_url: URL de l'image + form_url: + fields: + url: + label: URL de l'image + msg: + empty: L'URL de l'image ne peut pas être vide. + name: + label: Description + btn_cancel: Annuler + btn_confirm: Ajouter + uploading: Téléversement en cours + indent: + text: Indentation + outdent: + text: Désindenter + italic: + text: Mise en valeur + link: + text: Hyperlien + add_link: Ajouter un lien hypertexte + form: + fields: + url: + label: URL + msg: + empty: L'URL ne peut pas être vide. + name: + label: Description + btn_cancel: Annuler + btn_confirm: Ajouter + ordered_list: + text: Liste numérotée + unordered_list: + text: Liste à puces + table: + text: Tableau + heading: Titre + cell: Cellule + file: + text: Joindre des fichiers + not_supported: "Ne prenez pas en charge ce type de fichier. Réessayez avec {{file_type}}." + max_size: "La taille du fichier ne doit pas dépasser {{size}} Mo." + close_modal: + title: Je ferme ce post comme... + btn_cancel: Annuler + btn_submit: Valider + remark: + empty: Ne peut pas être vide. + msg: + empty: Veuillez sélectionner une raison. + report_modal: + flag_title: Je suis en train de signaler ce post comme... + close_title: Je ferme ce post comme... + review_question_title: Vérifier la question + review_answer_title: Vérifier la réponse + review_comment_title: Revoir le commentaire + btn_cancel: Annuler + btn_submit: Envoyer + remark: + empty: Ne peut pas être vide. + msg: + empty: Veuillez sélectionner une raison s'il vous plaît. + not_a_url: Le format de l'URL est incorrect. + url_not_match: L'origine de l'URL ne correspond pas au site web actuel. + tag_modal: + title: Créer un nouveau tag + form: + fields: + display_name: + label: Nom Affiché + msg: + empty: Le nom d'affichage ne peut être vide. + range: Le nom doit contenir moins de 35 caractères. + slug_name: + label: Limace d'URL + desc: Titre de 35 caractères maximum. + msg: + empty: L'URL ne peut pas être vide. + range: Titre de 35 caractères maximum. + character: Le slug d'URL contient un jeu de caractères non autorisé. + desc: + label: Description + revision: + label: Révision + edit_summary: + label: Modifier le résumé + placeholder: >- + Expliquez brièvement vos modifications (orthographe corrigée, grammaire corrigée, mise en forme améliorée) + btn_cancel: Annuler + btn_submit: Valider + btn_post: Publier un nouveau tag + tag_info: + created_at: Créé + edited_at: Modifié + history: Historique + synonyms: + title: Synonymes + text: Les tags suivants seront redistribués vers + empty: Aucun synonyme trouvé. + btn_add: Ajouter un synonyme + btn_edit: Modifier + btn_save: Enregistrer + synonyms_text: Les balises suivantes seront remappées en + delete: + title: Supprimer cette étiquette + tip_with_posts: >- +

Nous ne permettons pas la suppression d'un tag avec des posts

Veuillez d'abord supprimer ce tag des posts.

+ tip_with_synonyms: >- +

Nous ne permettons pas de supprimer un tag avec des synonymes.

Veuillez d'abord supprimer les synonymes de ce tag.

+ tip: Êtes-vous sûr de vouloir supprimer ? + close: Fermer + merge: + title: Étiquette de fusion + source_tag_title: Étiquette de source + source_tag_description: Cette étiquette de source et ses données associées seront réorganisées vers l'étiquette cible. + target_tag_title: Étiquette cible + target_tag_description: Un synonyme entre ces deux étiquettes sera créé après la fusion. + no_results: Aucune étiquette correspondante + btn_submit: Valider + btn_close: Fermer + edit_tag: + title: Editer le tag + default_reason: Éditer le tag + default_first_reason: Ajouter un tag + btn_save_edits: Enregistrer les modifications + btn_cancel: Annuler + dates: + long_date: D MMM + long_date_with_year: "D MMMM YYYY" + long_date_with_time: "D MMM YYYY [at] HH:mm" + now: maintenant + x_seconds_ago: "il y a {{count}}s" + x_minutes_ago: "il y a {{count}}m" + x_hours_ago: "il y a {{count}}h" + hour: heure + day: jour + hours: heures + days: jours + month: month + months: months + year: year + reaction: + heart: cœur + smile: sourire + frown: froncer les sourcils + btn_label: ajout et suppression de réactions + undo_emoji: annuler la réaction {{ emoji }} + react_emoji: réagir à {{ emoji }} + unreact_emoji: annuler la réaction avec {{ emoji }} + comment: + btn_add_comment: Ajoutez un commentaire + reply_to: Répondre à + btn_reply: Répondre + btn_edit: Éditer + btn_delete: Supprimer + btn_flag: Balise + btn_save_edits: Enregistrer les modifications + btn_cancel: Annuler + show_more: "{{count}} commentaires restants" + tip_question: >- + Utilisez les commentaires pour demander plus d'informations ou suggérer des améliorations. Évitez de répondre aux questions dans les commentaires. + tip_answer: >- + Utilisez des commentaires pour répondre à d'autres utilisateurs ou leur signaler des modifications. Si vous ajoutez de nouvelles informations, modifiez votre message au lieu de commenter. + tip_vote: Il ajoute quelque chose d'utile au post + edit_answer: + title: Modifier la réponse + default_reason: Modifier la réponse + default_first_reason: Ajouter une réponse + form: + fields: + revision: + label: Modification + answer: + label: Réponse + feedback: + characters: le contenu doit comporter au moins 6 caractères. + edit_summary: + label: Modifier le résumé + placeholder: >- + Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée) + btn_save_edits: Enregistrer les modifications + btn_cancel: Annuler + tags: + title: Étiquettes + sort_buttons: + popular: Populaire + name: Nom + newest: Le plus récent + button_follow: Suivre + button_following: Abonnements + tag_label: questions + search_placeholder: Filtrer par étiquette + no_desc: L'étiquette n'a pas de description. + more: Plus + wiki: Wiki + ask: + title: Create Question + edit_title: Modifier la question + default_reason: Modifier la question + default_first_reason: Create question + similar_questions: Questions similaires + form: + fields: + revision: + label: Modification + title: + label: Titre + placeholder: What's your topic? Be specific. + msg: + empty: Le titre ne peut pas être vide. + range: Titre de 150 caractères maximum + body: + label: Corps + msg: + empty: Le corps ne peut pas être vide. + tags: + label: Étiquettes + msg: + empty: Les étiquettes ne peuvent pas être vides. + answer: + label: Réponse + msg: + empty: La réponse ne peut être vide. + edit_summary: + label: Modifier le résumé + placeholder: >- + Expliquez brièvement vos changements (correction orthographique, correction grammaticale, mise en forme améliorée) + btn_post_question: Publier votre question + btn_save_edits: Enregistrer les modifications + answer_question: Répondre à votre propre question + post_question&answer: Publiez votre question et votre réponse + tag_selector: + add_btn: Ajouter une étiquette + create_btn: Créer une nouvelle étiquette + search_tag: Rechercher une étiquette + hint: "Describe what your content is about, at least one tag is required." + no_result: Aucune étiquette correspondante + tag_required_text: Étiquette requise (au moins une) + header: + nav: + question: Questions + tag: Étiquettes + user: Utilisateurs + badges: Badges + profile: Profil + setting: Paramètres + logout: Se déconnecter + admin: Administration + review: Vérifier + bookmark: Favoris + moderation: Modération + search: + placeholder: Rechercher + footer: + build_on: >- + Propulsé par <1> Apache Answer - le logiciel open-source qui alimente les communautés de Q&A.
Fait avec amour ©️ {{cc}}. + upload_img: + name: Remplacer + loading: chargement en cours... + pic_auth_code: + title: Captcha + placeholder: Saisissez le texte ci-dessus + msg: + empty: Le captcha ne peut pas être vide. + inactive: + first: >- + Vous avez presque fini ! Un mail de confirmation a été envoyé à {{mail}}. Veuillez suivre les instructions dans le mail pour activer votre compte. + info: "S'il n'arrive pas, vérifiez dans votre dossier spam." + another: >- + Nous vous avons envoyé un autre e-mail d'activation à {{mail}}. Cela peut prendre quelques minutes pour arriver ; assurez-vous de vérifier votre dossier spam. + btn_name: Renvoyer le mail d'activation + change_btn_name: Modifier l'e-mail + msg: + empty: Ne peut pas être vide. + resend_email: + url_label: Êtes-vous sûr de vouloir renvoyer l'email d'activation ? + url_text: Vous pouvez également donner le lien d'activation ci-dessus à l'utilisateur. + login: + login_to_continue: Connectez-vous pour continuer + info_sign: Vous n'avez pas de compte ? <1>Inscrivez-vous + info_login: Vous avez déjà un compte ? <1>Connectez-vous + agreements: En vous inscrivant, vous acceptez la <1>politique de confidentialité et les <3>conditions de service. + forgot_pass: Mot de passe oublié ? + name: + label: Nom + msg: + empty: Le nom ne peut pas être vide. + range: Le nom doit contenir entre 2 et 30 caractères. + character: 'Doit utiliser le jeu de caractères "a-z", "0-9", " - . _"' + email: + label: Email + msg: + empty: L'email ne peut pas être vide. + password: + label: Mot de passe + msg: + empty: Le mot de passe ne peut pas être vide. + different: Les mots de passe saisis ne sont pas identiques + account_forgot: + page_title: Mot de passe oublié + btn_name: Envoyer un e-mail de récupération + send_success: >- + Si un compte est associé à {{mail}}, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe. + email: + label: E-mail + msg: + empty: L'e-mail ne peut pas être vide. + change_email: + btn_cancel: Annuler + btn_update: Mettre à jour l'adresse e-mail + send_success: >- + Si un compte est associé à {{mail}}, vous recevrez un email contenant les instructions pour réinitialiser votre mot de passe. + email: + label: Nouvel e-mail + msg: + empty: L'email ne peut pas être vide. + oauth: + connect: Se connecter avec {{ auth_name }} + remove: Retirer {{ auth_name }} + oauth_bind_email: + subtitle: Ajoutez un e-mail de récupération à votre compte. + btn_update: Mettre à jour l'adresse e-mail + email: + label: Email + msg: + empty: L'email ne peut pas être vide. + modal_title: L'email existe déjà. + modal_content: Cette adresse e-mail est déjà enregistrée. Êtes-vous sûr de vouloir vous connecter au compte existant ? + modal_cancel: Modifier l'adresse e-mail + modal_confirm: Se connecter au compte existant + password_reset: + page_title: Réinitialiser le mot de passe + btn_name: Réinitialiser mon mot de passe + reset_success: >- + Vous avez modifié votre mot de passe avec succès ; vous allez être redirigé vers la page de connexion. + link_invalid: >- + Désolé, ce lien de réinitialisation de mot de passe n'est plus valide. Peut-être que votre mot de passe est déjà réinitialisé ? + to_login: Continuer vers la page de connexion + password: + label: Mot de passe + msg: + empty: Le mot de passe ne peut pas être vide. + length: La longueur doit être comprise entre 8 et 32 + different: Les mots de passe saisis ne sont pas identiques + password_confirm: + label: Confirmer le nouveau mot de passe + settings: + page_title: Paramètres + goto_modify: Aller modifier + nav: + profile: Profil + notification: Notifications + account: Compte + interface: Interface + profile: + heading: Profil + btn_name: Enregistrer + display_name: + label: Nom affiché + msg: Le nom ne peut être vide. + msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères. + username: + label: Nom d'utilisateur + caption: Les gens peuvent vous mentionner avec "@username". + msg: Le nom d'utilisateur ne peut pas être vide. + msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères. + character: 'Doit utiliser seulement les caractères "a-z", "0-9", " - . _"' + avatar: + label: Photo de profil + gravatar: Gravatar + gravatar_text: Vous pouvez modifier l'image sur + custom: Personnaliser + custom_text: Vous pouvez charger votre image. + default: Système + msg: Veuillez charger un avatar + bio: + label: Biographie + website: + label: Site Web + placeholder: "https://example.com" + msg: Format du site web incorrect + location: + label: Position + placeholder: "Ville, Pays" + notification: + heading: Notifications + turn_on: Activer + inbox: + label: Notifications par e-mail + description: Réponses à vos questions, commentaires, invitaitons et plus. + all_new_question: + label: Toutes les nouvelles questions + description: Recevez une notification pour toutes les nouvelles questions. Jusqu'à 50 questions par semaine. + all_new_question_for_following_tags: + label: Toutes les nouvelles questions pour les tags suivants + description: Recevez une notification pour toutes les nouvelles questions avec les tags suivants. + account: + heading: Compte + change_email_btn: Modifier l'adresse e-mail + change_pass_btn: Changer le mot de passe + change_email_info: >- + Nous vous avons envoyé un mail à cette adresse. Merci de suivre les instructions. + email: + label: Email + new_email: + label: Nouvel e-mail + msg: La nouvelle adresse e-mail ne peut pas être vide. + pass: + label: Mot de passe actuel + msg: Le mot de passe ne peut pas être vide. + password_title: Mot de passe + current_pass: + label: Mot de passe actuel + msg: + empty: Le mot de passe actuel ne peut pas être vide. + length: La longueur doit être comprise entre 8 et 32. + different: Le mot de passe saisi ne correspond pas. + new_pass: + label: Nouveau mot de passe + pass_confirm: + label: Confirmer le nouveau mot de passe + interface: + heading: Interface + lang: + label: Langue de l'interface + text: Langue de l'interface utilisateur. Cela changera lorsque vous rafraîchissez la page. + my_logins: + title: Mes identifiants + label: Connectez-vous ou inscrivez-vous sur ce site en utilisant ces comptes. + modal_title: Supprimer la connexion + modal_content: Confirmez-vous vouloir supprimer cette connexion de votre compte ? + modal_confirm_btn: Supprimer + remove_success: Supprimé avec succès + toast: + update: mise à jour effectuée + update_password: Mot de passe changé avec succès. + flag_success: Merci pour votre signalement. + forbidden_operate_self: Interdit d'opérer sur vous-même + review: Votre révision s'affichera après vérification. + sent_success: Envoyé avec succès + related_question: + title: Related + answers: réponses + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Personnes interrogées + desc: Invite people who you think might know the answer. + invite: Inviter à répondre + add: Ajouter des personnes + search: Rechercher des personnes + question_detail: + action: Action + Asked: Demandé + asked: demandé + update: Modifié + edit: modifié + commented: commenté + Views: Consultée + Follow: S’abonner + Following: Abonné(s) + follow_tip: Suivre cette question pour recevoir des notifications + answered: répondu + closed_in: Fermé dans + show_exist: Afficher la question existante. + useful: Utile + question_useful: C'est utile et clair + question_un_useful: Ce n'est pas clair ou n'est pas utile + question_bookmark: Ajouter cette question à vos favoris + answer_useful: C'est utile + answer_un_useful: Ce n'est pas utile + answers: + title: Réponses + score: Score + newest: Les plus récents + oldest: Le plus ancien + btn_accept: Accepter + btn_accepted: Accepté + write_answer: + title: Votre réponse + edit_answer: Modifier ma réponse existante + btn_name: Poster votre réponse + add_another_answer: Ajouter une autre réponse + confirm_title: Continuer à répondre + continue: Continuer + confirm_info: >- +

Êtes-vous sûr de vouloir ajouter une autre réponse ?

Vous pouvez utiliser le lien d'édition pour affiner et améliorer votre réponse existante.

+ empty: La réponse ne peut être vide. + characters: le contenu doit comporter au moins 6 caractères. + tips: + header_1: Merci pour votre réponse + li1_1: N’oubliez pas de répondre à la question. Fournissez des détails et partagez vos recherches. + li1_2: Sauvegardez toutes les déclarations que vous faites avec des références ou une expérience personnelle. + header_2: Mais évitez... + li2_1: Demander de l'aide, chercher des éclaircissements ou répondre à d'autres réponses. + reopen: + confirm_btn: Rouvrir + title: Rouvrir ce message + content: Êtes-vous sûr de vouloir rouvrir ? + list: + confirm_btn: Liste + title: Lister ce message + content: Êtes-vous sûr de vouloir lister ? + unlist: + confirm_btn: Délister + title: Masquer ce message de la liste + content: Êtes-vous sûr de vouloir masquer ce message de la liste ? + pin: + title: Épingler cet article + content: Êtes-vous sûr de vouloir l'épingler globalement ? Ce message apparaîtra en haut de toutes les listes de messages. + confirm_btn: Épingler + delete: + title: Supprimer la publication + question: >- + Nous ne recommandons pas de supprimer des questions avec des réponses car cela prive les futurs lecteurs de cette connaissance.

Suppression répétée des questions répondues peut empêcher votre compte de poser. Êtes-vous sûr de vouloir supprimer ? + answer_accepted: >- +

Nous ne recommandons pas de supprimer la réponse acceptée car cela prive les futurs lecteurs de cette connaissance.

La suppression répétée des réponses acceptées peut empêcher votre compte de répondre. Êtes-vous sûr de vouloir supprimer ? + other: Êtes-vous sûr de vouloir supprimer ? + tip_answer_deleted: Cette réponse a été supprimée + undelete_title: Annuler la suppression de ce message + undelete_desc: Êtes-vous sûr de vouloir annuler la suppression ? + btns: + confirm: Confimer + cancel: Annuler + edit: Modifier + save: Enregistrer + delete: Supprimer + undelete: Annuler la suppression + list: Liste + unlist: Délister + unlisted: Non listé + login: Se connecter + signup: S'inscrire + logout: Se déconnecter + verify: Vérifier + create: Créer + approve: Approuver + reject: Rejeter + skip: Ignorer + discard_draft: Abandonner le brouillon + pinned: Épinglé + all: Tous + question: Question + answer: Réponse + comment: Commentaire + refresh: Actualiser + resend: Renvoyer + deactivate: Désactiver + active: Actif + suspend: Suspendre + unsuspend: Lever la suspension + close: Fermer + reopen: Rouvrir + ok: OK + light: Clair + dark: Sombre + system_setting: Paramètres système + default: Défaut + reset: Réinitialiser + tag: Étiquette + post_lowercase: publier + filter: Filtre + ignore: Ignore + submit: Soumettre + normal: Normal + closed: Fermé + deleted: Supprimé + deleted_permanently: Supprimé définitivement + pending: En attente de traitement + more: Plus + view: Vue + card: Carte + compact: Compact + display_below: Afficher dessous + always_display: Toujours afficher + or: ou + back_sites: Retour aux sites + search: + title: Résultats de la recherche + keywords: Mots-clés + options: Options + follow: Suivre + following: Abonnements + counts: "{{count}} Résultats" + counts_loading: "... Results" + more: Plus + sort_btns: + relevance: Pertinence + newest: Les plus récents + active: Actif + score: Score + more: Plus + tips: + title: Astuces de recherche avancée + tag: "<1>[tag] recherche à l'aide d'un tag" + user: "<1>utilisateur:username recherche par auteur" + answer: "<1>réponses:0 questions sans réponses" + score: "<1>score:3 messages avec plus de 3 points" + question: "<1>est:question rechercher des questions" + is_answer: "<1>est :réponse réponses de recherche" + empty: Nous n'avons rien trouvé.
Essayez des mots-clés différents ou moins spécifiques. + share: + name: Partager + copy: Copier le lien + via: Partager via... + copied: Copié + facebook: Partager sur Facebook + twitter: Partager sur X + cannot_vote_for_self: Vous ne pouvez pas voter pour votre propre message. + modal_confirm: + title: Erreur... + delete_permanently: + title: Supprimer définitivement + content: Êtes-vous sûr de vouloir supprimer définitivement ? + account_result: + success: Votre nouveau compte est confirmé; vous serez redirigé vers la page d'accueil. + link: Continuer vers la page d'accueil + oops: Oups ! + invalid: Le lien que vous utilisez ne fonctionne plus. + confirm_new_email: Votre adresse email a été mise à jour. + confirm_new_email_invalid: >- + Désolé, ce lien de confirmation n'est plus valide. Votre email est peut-être déjà modifié ? + unsubscribe: + page_title: Se désabonner + success_title: Désabonnement réussi + success_desc: Vous avez été supprimé de cette liste d'abonnés avec succès et ne recevrez plus d'e-mails. + link: Modifier les paramètres + question: + following_tags: Hashtags suivis + edit: Éditer + save: Enregistrer + follow_tag_tip: Suivez les tags pour organiser votre liste de questions. + hot_questions: Questions populaires + all_questions: Toutes les questions + x_questions: "{{ count }} questions" + x_answers: "{{ count }} réponses" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Réponses + newest: Les plus récents + active: Actif + hot: Populaires + frequent: Fréquent + recommend: Recommandé + score: Score + unanswered: Sans réponse + modified: modifié + answered: répondu + asked: demandé + closed: fermé + follow_a_tag: Suivre ce tag + more: Plus + personal: + overview: Aperçu + answers: Réponses + answer: réponse + questions: Questions + question: question + bookmarks: Favoris + reputation: Réputation + comments: Commentaires + votes: Votes + badges: Badges + newest: Les plus récents + score: Score + edit_profile: Éditer le profil + visited_x_days: "Visité {{ count }} jours" + viewed: Vu + joined: Inscrit + comma: "," + last_login: Vu + about_me: À propos de moi + about_me_empty: "// Hello, World !" + top_answers: Les meilleures réponses + top_questions: Questions les plus populaires + stats: Statistiques + list_empty: Aucune publication trouvée.
Peut-être souhaiteriez-vous sélectionner un autre onglet ? + content_empty: Aucun post trouvé. + accepted: Accepté + answered: a répondu + asked: a demandé + downvoted: voté contre + mod_short: MOD + mod_long: Modérateurs + x_reputation: réputation + x_votes: votes reçus + x_answers: réponses + x_questions: questions + recent_badges: Badges récents + install: + title: Installation + next: Suivant + done: Terminé + config_yaml_error: Impossible de créer le fichier config.yaml. + lang: + label: Veuillez choisir une langue + db_type: + label: Moteur de base de données + db_username: + label: Nom d'utilisateur + placeholder: root + msg: Le nom d'utilisateur ne peut pas être vide. + db_password: + label: Mot de passe + placeholder: root + msg: Le mot de passe ne peut pas être vide. + db_host: + label: Hôte de la base de données + placeholder: "db:3306" + msg: L'hôte de la base de données ne peut pas être vide. + db_name: + label: Nom de la base de données + placeholder: réponse + msg: Le nom de la base de données ne peut pas être vide. + db_file: + label: Fichier de base de données + placeholder: /data/answer.db + msg: Le fichier de base de données ne doit pas être vide. + ssl_enabled: + label: Activer SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: Mode SSL + ssl_root_cert: + placeholder: Chemin du fichier sslrootcert + msg: Le chemin vers le fichier sslrootcert ne peut pas être vide + ssl_cert: + placeholder: Chemin du fichier sslcert + msg: Le chemin vers le fichier sslcert ne peut pas être vide + ssl_key: + placeholder: Chemin du fichier sslkey + msg: Le chemin vers le fichier sslkey ne peut pas être vide + config_yaml: + title: Créer config.yaml + label: Le fichier config.yaml a été créé. + desc: >- + Vous pouvez créer manuellement le fichier <1>config.yaml dans le répertoire <1>/var/wwww/xxx/ et y coller le texte suivant. + info: Après avoir fini, cliquez sur le bouton "Suivant". + site_information: Informations du site + admin_account: Compte Admin + site_name: + label: Nom du site + msg: Le nom ne peut pas être vide. + msg_max_length: Le nom affiché doit avoir une longueur de 4 à 30 caractères. + site_url: + label: URL du site + text: L'adresse de ce site. + msg: + empty: L'URL ne peut pas être vide. + incorrect: Le format de l'URL est incorrect. + max_length: L'URL du site doit avoir une longueur maximale de 512 caractères. + contact_email: + label: Email de contact + text: L'adresse email du responsable du site. + msg: + empty: L'email de contact ne peut pas être vide. + incorrect: Le format de l'email du contact est incorrect. + login_required: + label: Privé + switch: Connexion requise + text: Seuls les utilisateurs connectés peuvent accéder à cette communauté. + admin_name: + label: Nom + msg: Le nom ne peut pas être vide. + character: 'Utiliser seulement les caractères "a-z", "0-9", " - . _"' + msg_max_length: La longueur du nom doit être comprise entre 2 et 30 caractères. + admin_password: + label: Mot de passe + text: >- + Vous aurez besoin de ce mot de passe pour vous connecter . Sauvegarder le de façon sécurisée. + msg: Le mot de passe ne peut pas être vide. + msg_min_length: Le mot de passe doit comporter au moins 8 caractères. + msg_max_length: Le mot de passe doit comporter au maximum 32 caractères. + admin_confirm_password: + label: "Répétez le mot de passe" + text: "Veuillez saisir à nouveau votre mot de passe pour confirmer." + msg: "Les mots de passe ne correspondent pas." + admin_email: + label: Email + text: Vous aurez besoin de cet email pour vous connecter. + msg: + empty: L'email ne peut pas être vide. + incorrect: Le format de l'email est incorrect. + ready_title: Votre site est prêt + ready_desc: >- + Si vous avez envie de changer plus de paramètres, visitez la <1>section admin; retrouvez la dans le menu du site. + good_luck: "Amusez-vous et bonne chance !" + warn_title: Attention + warn_desc: >- + Le fichier <1>config.yaml existe déjà. Si vous avez besoin de réinitialiser l'un des éléments de configuration de ce fichier, veuillez le supprimer d'abord. + install_now: Vous pouvez essayer de <1>l'installer maintenant. + installed: Déjà installé + installed_desc: >- + Il semble que se soit déjà installer. Pour tout réinstaller, veuillez d'abord nettoyer votre ancienne base de données. + db_failed: La connexion à la base de données a échoué + db_failed_desc: >- + Cela signifie que les informations de la base de données dans votre fichier <1>config.yaml est incorrect ou le contact avec le serveur de base de données n'a pas pu être établi. Cela pourrait signifier que le serveur de base de données de votre hôte est hors service. + counts: + views: vues + votes: votes + answers: réponses + accepted: Accepté + page_error: + http_error: Erreur HTTP {{ code }} + desc_403: Vous n'avez pas l'autorisation d'accéder à cette page. + desc_404: Malheureusement, cette page n'existe pas. + desc_50X: Le serveur a rencontré une erreur et n'a pas pu répondre à votre requête. + back_home: Retour à la page d'accueil + page_maintenance: + desc: "Nous sommes en maintenance, nous serons bientôt de retour." + nav_menus: + dashboard: Tableau de bord + contents: Contenus + questions: Questions + answers: Réponses + users: Utilisateurs + badges: Badges + flags: Signalements + settings: Paramètres + general: Général + interface: Interface + smtp: SMTP + branding: Marque + legal: Légal + write: Écrire + tos: Conditions d'utilisation + privacy: Confidentialité + seo: SEO + customize: Personnaliser + themes: Thèmes + login: Se connecter + privileges: Privilèges + plugins: Extensions + installed_plugins: Extensions installées + apperance: Apparence + website_welcome: Bienvenue sur {{site_name}} + user_center: + login: Connexion + qrcode_login_tip: Veuillez utiliser {{ agentName }} pour scanner le code QR et vous connecter. + login_failed_email_tip: La connexion a échoué, veuillez autoriser cette application à accéder à vos informations de messagerie avant de réessayer. + badges: + modal: + title: Félicitations + content: Vous avez gagné un nouveau badge. + close: Fermer + confirm: Voir les badges + title: Badges + awarded: Octroyé + earned_×: Gagné ×{{ number }} + ×_awarded: "{{ number }} octroyés" + can_earn_multiple: Vous pouvez gagner cela plusieurs fois. + earned: Gagné + admin: + admin_header: + title: Admin + dashboard: + title: Tableau de bord + welcome: Bienvenue dans l'admin ! + site_statistics: Statistiques du site + questions: "Questions :" + resolved: "Résolu :" + unanswered: "Sans réponse :" + answers: "Réponses :" + comments: "Commentaires:" + votes: "Votes :" + users: "Utilisateurs :" + flags: "Signalements:" + reviews: "Revoir :" + site_health: Etat du site + version: "Version :" + https: "HTTPS :" + upload_folder: "Dossier de téléversement :" + run_mode: "Mode de fonctionnement :" + private: Privé + public: Public + smtp: "SMTP :" + timezone: "Fuseau horaire :" + system_info: Informations système + go_version: "Version de Go :" + database: "Base de donnée :" + database_size: "Taille de la base de données :" + storage_used: "Stockage utilisé :" + uptime: "Uptime :" + links: Liens + plugins: Extensions + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Commentaires + support: Support + review: Vérifier + config: Configuration + update_to: Mise à jour vers + latest: Récents + check_failed: Vérification échouée + "yes": "Oui" + "no": "Non" + not_allowed: Non autorisé + allowed: Autorisé + enabled: Activé + disabled: Désactivé + writable: Écriture autorisée + not_writable: Écriture refusée + flags: + title: Signalements + pending: En attente + completed: Complété + flagged: Signalé + flagged_type: Signalé {{ type }} + created: Créé + action: Action + review: Vérification + user_role_modal: + title: Changer le rôle d'un utilisateur en... + btn_cancel: Annuler + btn_submit: Valider + new_password_modal: + title: Définir un nouveau mot de passe + form: + fields: + password: + label: Mot de passe + text: L'utilisateur sera déconnecté et devra se connecter à nouveau. + msg: Le mot de passe doit contenir entre 8 et 32 caractères. + btn_cancel: Annuler + btn_submit: Envoyer + edit_profile_modal: + title: Éditer le profil + form: + fields: + display_name: + label: Nom affiché + msg_range: Le nom d'affichage doit contenir entre 2 et 30 caractères. + username: + label: Nom d'utilisateur + msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères. + email: + label: Email + msg_invalid: Adresse e-mail non valide. + edit_success: Modifié avec succès + btn_cancel: Annuler + btn_submit: Soumettre + user_modal: + title: Ajouter un nouvel utilisateur + form: + fields: + users: + label: Ajouter des utilisateurs en masse + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Séparez « nom, email, mot de passe » par des virgules. Un utilisateur par ligne. + msg: "Veuillez entrer l'email de l'utilisateur, un par ligne." + display_name: + label: Nom affiché + msg: Le nom affiché doit avoir une longueur de 2 à 30 caractères. + email: + label: Email + msg: L'email n'est pas valide. + password: + label: Mot de passe + msg: Le mot de passe doit comporter entre 8 et 32 caractères. + btn_cancel: Annuler + btn_submit: Valider + users: + title: Utilisateurs + name: Nom + email: E-mail + reputation: Réputation + created_at: Date de création + delete_at: Date de suppression + suspend_at: Date de suspension + suspend_until: Suspend until + status: Statut + role: Rôle + action: Action + change: Modifier + all: Tous + staff: Staff + more: Plus + inactive: Inactif + suspended: Suspendu + deleted: Supprimé + normal: Normal + Moderator: Modérateur + Admin: Administrateur + User: Utilisateur + filter: + placeholder: "Filtrer par nom, utilisateur:id" + set_new_password: Définir un nouveau mot de passe + edit_profile: Éditer le profil + change_status: Modifier le statut + change_role: Modifier le rôle + show_logs: Voir les logs + add_user: Ajouter un utilisateur + deactivate_user: + title: Désactiver l'utilisateur + content: Un utilisateur inactif doit revalider son email. + delete_user: + title: Supprimer cet utilisateur + content: Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est définitive ! + remove: Supprimer leur contenu + label: Supprimer toutes les questions, réponses, commentaires, etc. + text: Ne cochez pas cette case si vous souhaitez seulement supprimer le compte de l'utilisateur. + suspend_user: + title: Suspendre cet utilisateur + content: Un utilisateur suspendu ne peut pas se connecter. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Non listé + post: Publication + votes: Votes + answers: Réponses + created: Créé + status: Statut + action: Action + change: Modifier + pending: En attente de traitement + filter: + placeholder: "Filtrer par titre, question:id" + answers: + page_title: Réponses + post: Publication + votes: Votes + created: Créé + status: Statut + action: Action + change: Modifier + filter: + placeholder: "Filtrer par titre, question:id" + general: + page_title: Général + name: + label: Nom du site + msg: Le nom ne peut pas être vide. + text: "Le nom de ce site, tel qu'il est utilisé dans la balise titre." + site_url: + label: URL du site + msg: L'URL ne peut pas être vide. + validate: Indiquez une URL valide. + text: L'adresse de ce site. + short_desc: + label: Courte description du site + msg: La description courte ne peut pas être vide. + text: "La description courte, telle qu'elle est utilisée dans le tag titre de la page d'accueil." + desc: + label: Description du site + msg: La description du site ne peut pas être vide. + text: "Décrivez ce site en une phrase, telle qu'elle est utilisée dans la balise meta description." + contact_email: + label: Email du contact + msg: L'email de contact ne peut pas être vide. + validate: L'email de contact n'est pas valide. + text: L'adresse email du responsable du site. + check_update: + label: Mises à jour logicielles + text: Rechercher automatiquement les mises à jour + interface: + page_title: Interface + language: + label: Langue de l'interface + msg: La langue de l'interface ne peut pas être vide. + text: Langue de l'interface de l'utilisateur. Cela changera lorsque vous rafraîchissez la page. + time_zone: + label: Fuseau Horaire + msg: Le fuseau horaire ne peut pas être vide. + text: Choisissez une ville dans le même fuseau horaire que vous. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: E-mail de l'expéditeur + msg: L'email expéditeur ne peut pas être vide. + text: L'adresse email à partir de laquelle les emails sont envoyés. + from_name: + label: Nom de l'expéditeur + msg: Le nom expéditeur ne peut pas être vide. + text: Le nom d'expéditeur à partir duquel les emails sont envoyés. + smtp_host: + label: Serveur SMTP + msg: Le'hôte SMTP ne peut pas être vide. + text: Votre serveur de mail. + encryption: + label: Chiffrement + msg: Le chiffrement ne peut pas être vide. + text: Pour la plupart des serveurs, l'option SSL est recommandée. + ssl: SSL + tls: TLS + none: Aucun + smtp_port: + label: Port SMTP + msg: Le port SMTP doit être compris entre 1 et 65535. + text: Le port vers votre serveur d'email. + smtp_username: + label: Utilisateur SMTP + msg: Le nom d'utilisateur SMTP ne peut pas être vide. + smtp_password: + label: Mot de passe SMTP + msg: Le mot de passe SMTP ne peut être vide. + test_email_recipient: + label: Destinataires des e-mails de test + text: Indiquez l'adresse email qui recevra l'email de test. + msg: Le destinataire de l'email de test est invalide + smtp_authentication: + label: Activer l'authentification + title: Authentification SMTP + msg: L'authentification SMTP ne peut pas être vide. + "yes": "Oui" + "no": "Non" + branding: + page_title: Marque + logo: + label: Logo + msg: Le logo ne peut pas être vide. + text: L'image du logo en haut à gauche de votre site. Utilisez une grande image rectangulaire avec une hauteur de 56 et un ratio d'aspect supérieur à 3:1. Si laissé vide, le titre du site sera affiché. + mobile_logo: + label: Logo pour la version mobile + text: Le logo utilisé sur la version mobile de votre site. Utilisez une image rectangulaire large avec une hauteur de 56. Si laissé vide, l'image du paramètre « logo » sera utilisée. + square_icon: + label: Icône carrée + msg: L'icône carrée ne peut pas être vide. + text: Image utilisée comme base pour les icônes de métadonnées. Idéalement supérieure à 512x512. + favicon: + label: Favicon + text: Une favicon pour votre site. Pour fonctionner correctement sur un CDN, il doit s'agir d'un png. Sera redimensionné en 32x32. Si laissé vide, « icône carrée » sera utilisé. + legal: + page_title: Légal + terms_of_service: + label: Conditions d’utilisation + text: "Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici." + privacy_policy: + label: Protection des données + text: "Vous pouvez ajouter le contenu des conditions de service ici. Si vous avez déjà un document hébergé ailleurs, veuillez fournir l'URL complète ici." + external_content_display: + label: Contenu externe + text: "Le contenu comprend des images, des vidéos et des médias intégrés à partir de sites web externes." + always_display: Toujours afficher le contenu externe + ask_before_display: Demander avant d'afficher le contenu externe + write: + page_title: Écrire + restrict_answer: + title: Écriture de la réponse + label: Chaque utilisateur ne peut écrire qu'une seule réponse pour chaque question + text: "Désactivez pour permettre aux utilisateurs d'écrire plusieurs réponses à la même question, ce qui peut causer une perte de concentration des réponses." + recommend_tags: + label: Tags recommandés + text: "Les balises recommandées apparaîtront par défaut dans la liste déroulante." + msg: + contain_reserved: "les tags recommandés ne peuvent pas contenir de tags réservés" + required_tag: + title: Définir les tags nécessaires + label: Définir les balises « Recommander» comme balises requises + text: "Chaque nouvelle question doit avoir au moins un tag recommandé." + reserved_tags: + label: Tags réservés + text: "Les tags réservés ne peuvent être ajoutés à un message que par un modérateur." + image_size: + label: Taille maximale de l'image (MB) + text: "La taille maximale de téléchargement d'image." + attachment_size: + label: Taille maximale des pièces jointes (MB) + text: "La taille maximale de téléchargement des fichiers joints." + image_megapixels: + label: Max mégapixels image + text: "Nombre maximum de mégapixels autorisés pour une image." + image_extensions: + label: Extensions de pièces jointes autorisées + text: "Une liste d'extensions de fichier autorisées pour l'affichage d'image, séparées par des virgules." + attachment_extensions: + label: Extensions de pièces jointes autorisées + text: "Une liste d'extensions de fichier autorisées pour le téléchargement, séparées par des virgules. ATTENTION : Autoriser les envois peut causer des problèmes de sécurité." + seo: + page_title: Référencement + permalink: + label: Lien permanent + text: Des structures d'URL personnalisées peuvent améliorer la facilité d'utilisation et la compatibilité de vos liens. + robots: + label: robots.txt + text: Ceci remplacera définitivement tous les paramètres liés au site. + themes: + page_title: Thèmes + themes: + label: Thèmes + text: Sélectionne un thème existant. + color_scheme: + label: Jeu de couleurs + navbar_style: + label: Style d'arrière-plan de la barre de navigation + primary_color: + label: Couleur primaire + text: Modifier les couleurs utilisées par vos thèmes + css_and_html: + page_title: CSS et HTML + custom_css: + label: CSS personnalisé + text: > + + head: + label: Head + text: > + + header: + label: En-tête + text: > + + footer: + label: Pied de page + text: Ceci va être inséré avant </html>. + sidebar: + label: Panneau latéral + text: Cela va être inséré dans la barre latérale. + login: + page_title: Se connecter + membership: + title: Adhésion + label: Autoriser les inscriptions + text: Désactivez pour empêcher quiconque de créer un nouveau compte. + email_registration: + title: Inscription par e-mail + label: Autoriser l'inscription par e-mail + text: Désactiver pour empêcher toute personne de créer un nouveau compte par e-mail. + allowed_email_domains: + title: Domaines d'email autorisés + text: Domaines de messagerie avec lesquels les utilisateurs peuvent créer des comptes. Un domaine par ligne. Ignoré si vide. + private: + title: Privé + label: Connexion requise + text: Seuls les utilisateurs connectés peuvent accéder à cette communauté. + password_login: + title: Connexion par mot de passe + label: Autoriser la connexion par e-mail et mot de passe + text: "AVERTISSEMENT : Si cette option est désactivée, vous ne pourrez peut-être pas vous connecter si vous n'avez pas configuré une autre méthode de connexion." + installed_plugins: + title: Extensions installées + plugin_link: Les plugins étendent les fonctionnalités d'Answer. Vous pouvez trouver des plugins dans le dépôt <1>Answer Plugin Repositor. + filter: + all: Tous + active: Actif + inactive: Inactif + outdated: Est obsolète + plugins: + label: Extensions + text: Sélectionnez une extension existante. + name: Nom + version: Versión + status: Statut + action: Action + deactivate: Désactiver + activate: Activer + settings: Paramètres + settings_users: + title: Utilisateurs + avatar: + label: Photo de profil par défaut + text: Pour les utilisateurs sans avatar personnalisé. + gravatar_base_url: + label: Gravatar Base URL + text: URL de la base de l'API du fournisseur Gravatar. Ignorée lorsqu'elle est vide. + profile_editable: + title: Profil modifiable + allow_update_display_name: + label: Permettre aux utilisateurs de changer leur nom d'affichage + allow_update_username: + label: Permettre aux clients de changer leurs noms d'utilisateur + allow_update_avatar: + label: Permettre aux utilisateurs de changer leur image de profil + allow_update_bio: + label: Permettre aux utilisateurs de changer leur biographie + allow_update_website: + label: Permettre aux utilisateurs de modifier leur site web + allow_update_location: + label: Permettre aux utilisateurs de modifier leur position + privilege: + title: Privilèges + level: + label: Niveau de réputation requis + text: Choisissez la réputation requise pour les privilèges + msg: + should_be_number: l'entrée doit être un nombre + number_larger_1: le nombre doit être égal ou supérieur à 1 + badges: + action: Action + active: Actif + activate: Activer + all: Tous + awards: Récompenses + deactivate: Désactiver + filter: + placeholder: Filtrer par nom, badge:id + group: Groupe + inactive: Inactif + name: Nom + show_logs: Voir les logs + status: Statut + title: Badges + form: + optional: (optionnel) + empty: ne peut pas être vide + invalid: est invalide + btn_submit: Sauvegarder + not_found_props: "La propriété requise {{ key }} est introuvable." + select: Sélectionner + page_review: + review: Vérifier + proposed: proposé + question_edit: Modifier la question + answer_edit: Modifier la réponse + tag_edit: Modifier le tag + edit_summary: Modifier le résumé + edit_question: Modifier la question + edit_answer: Modifier la réponse + edit_tag: Modifier l’étiquette + empty: Aucune révision restante. + approve_revision_tip: Acceptez-vous cette révision? + approve_flag_tip: Acceptez-vous ce rapport ? + approve_post_tip: Acceptez-vous ce post? + approve_user_tip: Acceptez-vous cet utilisateur ? + suggest_edits: Modifications suggérées + flag_post: Signaler ce message + flag_user: Signaler un utilisateur + queued_post: Message en file d'attente + queued_user: Utilisateur en file d'attente + filter_label: Type + reputation: réputation + flag_post_type: A signalé ce message comme {{ type }}. + flag_user_type: A signalé cet utilisateur comme {{ type }}. + edit_post: Éditer le post + list_post: Lister le post + unlist_post: Masquer la post de la liste + timeline: + undeleted: restauré + deleted: supprimé + downvote: vote négatif + upvote: voter pour + accept: accepté + cancelled: annulé + commented: commenté + rollback: Retour arrière (Rollback) + edited: modifié + answered: répondu + asked: demandé + closed: fermé + reopened: réouvert + created: créé + pin: épinglé + unpin: non épinglé + show: listé + hide: non listé + title: "Historique de" + tag_title: "Chronologie de" + show_votes: "Afficher les votes" + n_or_a: N/A + title_for_question: "Chronologie de" + title_for_answer: "Chronologie de la réponse à {{ title }} par {{ author }}" + title_for_tag: "Chronologie pour le tag" + datetime: Date et heure + type: Type + by: Par + comment: Commentaire + no_data: "Nous n'avons rien pu trouver." + users: + title: Utilisateurs + users_with_the_most_reputation: Utilisateurs ayant le score de réputation le plus élevé cette semaine + users_with_the_most_vote: Utilisateurs qui ont le plus voté cette semaine + staffs: Staff de la communauté + reputation: réputation + votes: votes + prompt: + leave_page: Voulez-vous vraiment quitter la page ? + changes_not_save: Impossible d'enregistrer vos modifications. + draft: + discard_confirm: Êtes-vous sûr de vouloir abandonner ce brouillon ? + messages: + post_deleted: Ce message a été supprimé. + post_cancel_deleted: Ce post a été restauré. + post_pin: Ce message a été épinglé. + post_unpin: Ce message a été déépinglé. + post_hide_list: Ce message a été masqué de la liste. + post_show_list: Ce message a été affiché dans la liste. + post_reopen: Ce message a été rouvert. + post_list: Ce post a été ajouté à la liste. + post_unlist: Ce post a été retiré de la liste. + post_pending: Votre message est en attente de révision. C'est un aperçu, il sera visible une fois qu'il aura été approuvé. + post_closed: Ce post a été fermé. + answer_deleted: Cette réponse a été supprimée. + answer_cancel_deleted: Cette réponse a été restaurée. + change_user_role: Le rôle de cet utilisateur a été modifié. + user_inactive: Cet utilisateur est déjà inactif. + user_normal: Cet utilisateur est déjà normal. + user_suspended: Cet utilisateur a été suspendu. + user_deleted: Cet utilisateur a été supprimé. + badge_activated: Ce badge a été activé. + badge_inactivated: Ce badge a été désactivé. + users_deleted: Ces utilisateurs ont été supprimés. + posts_deleted: Ces questions ont été supprimées. + answers_deleted: Ces réponses ont été supprimées. + copy: Copier dans le presse-papier + copied: Copié + external_content_warning: Les images/médias externes ne sont pas affichés. + + diff --git a/i18n/he_IL.yaml b/i18n/he_IL.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/he_IL.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/hi_IN.yaml b/i18n/hi_IN.yaml new file mode 100644 index 000000000..c26f39921 --- /dev/null +++ b/i18n/hi_IN.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: List + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: Email + e_mail: + other: Email + password: + other: Password + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Email and password do not match. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Edit Tag + ask_a_question: Create Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + new_alerts: New alerts + all_read: Mark all as read + show_more: Show more + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/hu_HU.yaml b/i18n/hu_HU.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/hu_HU.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/hy_AM.yaml b/i18n/hy_AM.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/hy_AM.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/i18n.go b/i18n/i18n.go index 0613302af..cbd5686cc 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package i18n import "embed" diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml new file mode 100644 index 000000000..7abb748db --- /dev/null +++ b/i18n/i18n.yaml @@ -0,0 +1,64 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# all support language +language_options: + - label: "English" + value: "en_US" + progress: 100 + - label: "Español" + value: "es_ES" + progress: 96 + - label: "Português(BR)" + value: "pt_BR" + progress: 96 + - label: "Português" + value: "pt_PT" + progress: 96 + - label: "Deutsch" + value: "de_DE" + progress: 96 + - label: "Français" + value: "fr_FR" + progress: 96 + - label: "日本語" + value: "ja_JP" + progress: 96 + - label: "Italiano" + value: "it_IT" + progress: 96 + - label: "Русский" + value: "ru_RU" + progress: 80 + - label: "简体中文" + value: "zh_CN" + progress: 100 + - label: "繁體中文" + value: "zh_TW" + progress: 47 + - label: "한국어" + value: "ko_KR" + progress: 73 + - label: "Tiếng Việt" + value: "vi_VN" + progress: 96 + - label: "Slovak" + value: "sk_SK" + progress: 45 + - label: "فارسی" + value: "fa_IR" + progress: 69 diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml new file mode 100644 index 000000000..6ff10273b --- /dev/null +++ b/i18n/id_ID.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Sukses. + unknown: + other: Kesalahan tidak diketahui. + request_format_error: + other: Permintaan tidak sah. + unauthorized_error: + other: Tidak diizinkan. + database_error: + other: Kesalahan data server. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Ubah + delete: + other: Hapus + close: + other: Tutup + reopen: + other: Buka kembali + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: Daftar + invite_someone_to_answer: + other: Undang seseorang untuk menjawab + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: Pengguna + admin: + other: Administrator + moderator: + other: Moderator + description: + user: + other: Default tanpa akses khusus. + admin: + other: Memiliki kontrol penuh atas situs. + moderator: + other: Memiliki kendali atas semua kiriman kecuali pengaturan administrator. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Tanyakan sesuatu + rank_answer_add_label: + other: Tulis jawaban + rank_comment_add_label: + other: Tulis komentar + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Kirim lebih dari 2 tautan secara bersamaan + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Undang seseorang untuk menjawab + rank_tag_add_label: + other: + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: + e_mail: + other: Email + password: + other: Kata sandi + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: '"Email" dan kata sandi tidak cocok.' + error: + common: + invalid_url: + other: URL salah. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password tidak boleh mengandung spasi. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email dan kata sandi tidak cocok. + answer: + not_found: + other: Jawaban tidak ditemukan. + cannot_deleted: + other: Tidak memiliki izin untuk menghapus. + cannot_update: + other: Tidak memiliki izin untuk memperbarui. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Tidak diizinkan untuk mengubah komentar. + not_found: + other: Komentar tidak ditemukan. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email telah terdaftar. + need_to_be_verified: + other: Email harus terverifikasi. + verify_url_expired: + other: URL verifikasi email telah kadaluwarsa, silahkan kirim ulang. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Bahasa tidak ditemukan. + object: + captcha_verification_failed: + other: Captcha salah. + disallow_follow: + other: Anda tidak diizinkan untuk mengikuti. + disallow_vote: + other: Anda tisak diizinkan untuk melakukan vote. + disallow_vote_your_self: + other: Anda tidak dapat melakukan voting untuk ulasan Anda sendiri. + not_found: + other: Objek tidak ditemukan. + verification_failed: + other: Verifikasi gagal. + email_or_password_incorrect: + other: Email dan kata sandi tidak cocok. + old_password_verification_failed: + other: Verifikasi password lama, gagal + new_password_same_as_previous_setting: + other: Kata sandi baru sama dengan kata sandi yang sebelumnya. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Pertanyaan tidak ditemukan. + cannot_deleted: + other: Tidak memiliki izin untuk menghapus. + cannot_close: + other: Tidak diizinkan untuk menutup. + cannot_update: + other: Tidak diizinkan untuk memperbarui. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Laporan penanganan gagal. + not_found: + other: Laporan tidak ditemukan. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag tidak ditemukan. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Silahkan isi setidaknya satu tag yang diperlukan. + not_contain_synonym_tags: + other: Tidak boleh mengandung Tag sinonim. + cannot_update: + other: Tidak memiliki izin untuk memperbaharui. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: Anda tidak bisa menetapkan sinonim dari tag saat ini dengan tag yang sama. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Tema tidak ditemukan. + revision: + review_underway: + other: Tidak dapat mengedit saat ini, sedang ada review versi pada antrian. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email dan kata sandi tidak cocok. + not_found: + other: Pengguna tidak ditemukan. + suspended: + other: Pengguna ini telah ditangguhkan. + username_invalid: + other: Nama pengguna tidak sesuai. + username_duplicate: + other: Nama pengguna sudah digunakan. + set_avatar: + other: Set avatar gagal. + cannot_update_your_role: + other: Anda tidak dapat mengubah peran anda sendiri. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Gagal membaca konfigurasi + database: + connection_failed: + other: Koneksi ke database gagal + create_table_failed: + other: Gagal membuat tabel + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: spam + desc: + other: Pertanyaan ini telah ditanyakan sebelumnya dan sudah ada jawabannya. + guideline: + name: + other: a community-specific reason + desc: + other: Pertanyaan ini tidak sesuai dengan pedoman komunitas. + multiple: + name: + other: membutuhkan detail atau kejelasan + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: lainnya + desc: + other: Posting ini membutuhkan alasan lain yang tidak tercantum di atas. + operation_type: + asked: + other: ditanyakan + answered: + other: dijawab + modified: + other: dimodifikasi + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: pertanyaan yang diperbaharui + answer_the_question: + other: pertanyaan yang dijawab + update_answer: + other: jawaban yang diperbaharui + accept_answer: + other: pertanyaan yanag diterima + comment_question: + other: pertanyaan yang dikomentari + comment_answer: + other: jawaban yang dikomentari + reply_to_you: + other: membalas Anda + mention_you: + other: menyebutmu + your_question_is_closed: + other: Pertanyaanmu telah ditutup + your_question_was_deleted: + other: Pertanyaanmu telah dihapus + your_answer_was_deleted: + other: Jawabanmu telah dihapus + your_comment_was_deleted: + other: Komentarmu telah dihapus + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Cara memformat + desc: >- + + pagination: + prev: Sebelumnya + next: Selanjutnya + page_title: + question: Pertanyaan + questions: Pertanyaan + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Ubah Tag + ask_a_question: Create Question + edit_question: Sunting Pertanyaan + edit_answer: Sunting jawaban + search: Cari + posts_containing: Postingan mengandung + settings: Pengaturan + notifications: Pemberitahuan + login: Log In + sign_up: Daftar + account_recovery: Pemulihan Akun + account_activation: Aktivasi Akun + confirm_email: Konfirmasi email + account_suspended: Akun Ditangguhkan + admin: Admin + change_email: Modifikasi email + install: Instalasi Answer + upgrade: Meng-upgrade Answer + maintenance: Pemeliharaan Website + users: Pengguna + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Pemberitahuan + inbox: Kotak Masuk + achievement: Pencapaian + new_alerts: New alerts + all_read: Tandai Semua Jika Sudah Dibaca + show_more: Tampilkan lebih banyak + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Akun Anda telah ditangguhkan + until_time: "Akun anda ditangguhkan sampai {{ time }}." + forever: Pengguna ini ditangguhkan selamanya. + end: Anda tidak sesuai dengan syarat pedoman komunitas. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Diagram alir + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Tambahkan sample code + form: + fields: + code: + label: Code + msg: + empty: Code tidak boleh kosong. + language: + label: Language + placeholder: Deteksi otomatis + btn_cancel: Batal + btn_confirm: Tambah + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Gambar + add_image: Tambahkan gambar + tab_image: Unggah gambar + form_image: + fields: + file: + label: Image file + btn: Pilih gambar + msg: + empty: File tidak boleh kosong. + only_image: Hanya file Gambar yang diperbolehkan. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: URL gambar + form_url: + fields: + url: + label: URL gambar + msg: + empty: URL gambar tidak boleh kosong. + name: + label: Description + btn_cancel: Batal + btn_confirm: Tambah + uploading: Sedang mengunggah + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Batal + btn_confirm: Tambah + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Postingan ini saya tutup sebagai... + btn_cancel: Batal + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Dibuat + edited_at: Disunting + history: Riwayat + synonyms: + title: Sinonim + text: Tag berikut akan dipetakan ulang ke + empty: Sinonim tidak ditemukan. + btn_add: Tambahkan sinonim + btn_edit: Sunting + btn_save: Simpan + synonyms_text: Tag berikut akan dipetakan ulang ke + delete: + title: Hapus tagar ini + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Tutup + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Ubah Tag + default_reason: Sunting tag + default_first_reason: Add tag + btn_save_edits: Simpan suntingan + btn_cancel: Batal + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: sekarang + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: jam + day: hari + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Tambahkan Komentar + reply_to: Balas ke + btn_reply: Balas + btn_edit: Sunting + btn_delete: Hapus + btn_flag: Flag + btn_save_edits: Simpan suntingan + btn_cancel: Batal + show_more: "{{count}} more comments" + tip_question: >- + Gunakan komentar untuk meminta informasi lebih lanjut atau menyarankan perbaikan. Hindari menjawab pertanyaan di komentar. + tip_answer: >- + Gunakan komentar untuk membalas pengguna lain atau memberi tahu mereka tentang perubahan. Jika Anda menambahkan informasi baru, cukup edit posting Anda. + tip_vote: It adds something useful to the post + edit_answer: + title: Sunting jawaban + default_reason: Edit jawaban + default_first_reason: Add answer + form: + fields: + revision: + label: Revisi + answer: + label: Jawaban + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Pengguna + badges: Badges + profile: Profil + setting: Pengaturan + logout: Keluar + admin: Admin + review: Ulasan + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Cari + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Ubah + loading: sedang memuat... + pic_auth_code: + title: Capthcha + placeholder: Masukkan teks di atas + msg: + empty: Captcha tidak boleh kosong. + inactive: + first: >- + Kamu hampir selesai! Kami telah mengirimkan email aktivasi ke {{mail}}. Silakan ikuti petunjuk dalam email untuk mengaktifkan akun Anda. + info: "Jika tidak ada email masuk, mohon periksa folder spam Anda." + another: >- + Kami telah mengirimkan email aktivasi lain kepada Anda di {{mail}}. Mungkin butuh beberapa menit untuk tiba; pastikan untuk memeriksa folder spam Anda. + btn_name: Kirim ulang email aktivasi + change_btn_name: Ganti email + msg: + empty: Tidak bisa kosong. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Masuk untuk melanjutkan + info_sign: Belum punya akun? <1>Daftar + info_login: Sudah punya akun? <1>Masuk + agreements: Dengan mendaftar, Anda menyetujui <1>kebijakan privasi dan <3>persyaratan layanan. + forgot_pass: Lupa password? + name: + label: Nama + msg: + empty: Nama tidak boleh kosong. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email tidak boleh kosong. + password: + label: Kata sandi + msg: + empty: Kata sandi tidak boleh kosong. + different: Kata sandi yang dimasukkan tidak sama + account_forgot: + page_title: Lupa kata sandi Anda + btn_name: Tulis email pemulihan + send_success: >- + Jika akun cocok dengan {{mail}}, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi. + email: + label: Email + msg: + empty: Email tidak boleh kosong. + change_email: + btn_cancel: Batal + btn_update: Perbarui alamat email + send_success: >- + Jika akun cocok dengan {{mail}}, Anda akan segera menerima email berisi petunjuk tentang cara menyetel ulang sandi. + email: + label: New email + msg: + empty: Email tidak boleh kosong. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Atur ulang kata sandi + btn_name: Atur ulang kata sandi saya + reset_success: >- + Anda berhasil mengubah kata sandi Anda; Anda akan dialihkan ke halaman login. + link_invalid: >- + Maaf, link setel ulang sandi ini sudah tidak valid. Mungkin kata sandi Anda sudah diatur ulang? + to_login: Lanjutkan ke halaman Login + password: + label: Kata sandi + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: Bahasa antarmuka pengguna. Itu akan berubah ketika Anda me-refresh halaman. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: pembaruan sukses + update_password: Kata sandi berhasil diganti. + flag_success: Terima kasih telah menandai. + forbidden_operate_self: Dilarang melakukan operasi ini pada diri sendiri + review: Revisi Anda akan ditampilkan setelah ditinjau. + sent_success: Sent successfully + related_question: + title: Related + answers: jawaban + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Ditanyakan + asked: ditanyakan + update: Diubah + edit: disunting + commented: commented + Views: Dilihat + Follow: Ikuti + Following: Mengikuti + follow_tip: Follow this question to receive notifications + answered: dijawab + closed_in: Ditutup pada + show_exist: Gunakan pertanyaan yang sudah ada. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Jawaban + score: Nilai + newest: Terbaru + oldest: Oldest + btn_accept: Terima + btn_accepted: Diterima + write_answer: + title: Jawaban Anda + edit_answer: Edit my existing answer + btn_name: Kirimkan jawaban Anda + add_another_answer: Tambahkan jawaban lain + confirm_title: Lanjutkan menjawab + continue: Lanjutkan + confirm_info: >- +

Yakin ingin menambahkan jawaban lain?

Sebagai gantinya, Anda dapat menggunakan tautan edit untuk menyaring dan menyempurnakan jawaban anda.

+ empty: Jawaban tidak boleh kosong. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Buka kembali postingan ini + content: Kamu yakin ingin membuka kembali? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Hapus pos ini + question: >- + Kami tidak menyarankan menghapus pertanyaan dengan jawaban karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.

Penghapusan berulang atas pertanyaan yang dijawab dapat mengakibatkan akun Anda diblokir untuk bertanya. Apakah Anda yakin ingin menghapus? + answer_accepted: >- +

Kami tidak menyarankan menghapus jawaban yang diterima karena hal itu menghilangkan pengetahuan ini dari pembaca di masa mendatang.

Penghapusan berulang dari jawaban yang diterima dapat menyebabkan akun Anda diblokir dari menjawab. Apakah Anda yakin ingin menghapus? + other: Anda yakin ingin menghapusnya? + tip_answer_deleted: Jawaban ini telah dihapus + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Konfirmasi + cancel: Batal + edit: Edit + save: Simpan + delete: Hapus + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Masuk + signup: Daftar + logout: Keluar + verify: Verifikasi + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Jawaban + newest: Terbaru + active: Aktif + hot: Hot + frequent: Frequent + recommend: Recommend + score: Nilai + unanswered: Belum dijawab + modified: diubah + answered: dijawab + asked: ditanyakan + closed: ditutup + follow_a_tag: Ikuti tagar + more: Lebih + personal: + overview: Ringkasan + answers: Jawaban + answer: jawaban + questions: Pertanyaan + question: pertanyaan + bookmarks: Bookmarks + reputation: Reputasi + comments: Komentar + votes: Vote + badges: Badges + newest: Terbaru + score: Nilai + edit_profile: Edit profile + visited_x_days: "Dikunjungi {{ count }} hari" + viewed: Dilihat + joined: Bergabung + comma: "," + last_login: Dilihat + about_me: Tentang Saya + about_me_empty: "// Hello, World !" + top_answers: Jawaban terpopuler + top_questions: Pertanyaan terpopuler + stats: Statistik + list_empty: Postingan tidak ditemukan.
Mungkin Anda ingin memilih tab lain? + content_empty: No posts found. + accepted: Diterima + answered: dijawab + asked: ditanyakan + downvoted: downvoted + mod_short: MOD + mod_long: Moderator + x_reputation: reputasi + x_votes: vote diterima + x_answers: jawaban + x_questions: pertanyaan + recent_badges: Recent Badges + install: + title: Installation + next: Selanjutnya + done: Selesai + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Koneksi ke database gagal + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dasbor + contents: Konten + questions: Pertanyaan + answers: Jawaban + users: Pengguna + badges: Badges + flags: Flags + settings: Pengaturan + general: Umum + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privasi + seo: SEO + customize: Kostumisasi + themes: Tema + login: Masuk + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dasbor + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Pertanyaan:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Jawaban:" + comments: "Komentar:" + votes: "Vote:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Versi:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Zona Waktu:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Penyimpanan yang terpakai:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Dokumen + feedback: Masukan + support: Dukungan + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Ubah + all: Semua + staff: Staf + more: More + inactive: Tidak Aktif + suspended: Ditangguhkan + deleted: Dihapus + normal: Normal + Moderator: Moderator + Admin: Admin + User: Pengguna + filter: + placeholder: "Filter berdasarkan nama, user:id" + set_new_password: Atur password baru + edit_profile: Edit profile + change_status: Ubah status + change_role: Ubah role + show_logs: Tampilkan log + add_user: Tambahkan pengguna + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Pertanyaan + unlisted: Unlisted + post: Post + votes: Vote + answers: Jawaban + created: Dibuat + status: Status + action: Action + change: Ubah + pending: Pending + filter: + placeholder: "Filter berdasarkan judul, question:id" + answers: + page_title: Jawaban + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Enkripsi + msg: Enkripsi tidak boleh kosong. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: Tidak ada + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: Port untuk server email Anda. + smtp_username: + label: SMTP username + msg: Nama Pengguna SMTP tidak boleh kosong. + smtp_password: + label: SMTP password + msg: Password SMTP tidak boleh kosong. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar Base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 0b9439412..8b8fd71c5 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -1,170 +1,2341 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" - email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" - report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" - -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: +# The following fields are used for back-end +backend: + base: + success: + other: Successo. + unknown: + other: Errore sconosciuto. + request_format_error: + other: Il formato della richiesta non è valido. + unauthorized_error: + other: Non autorizzato. + database_error: + other: Errore nel server dati. + forbidden_error: + other: Vietato. + duplicate_request_error: + other: Duplica invio. + action: + report: + other: Segnala + edit: + other: Modifica + delete: + other: Cancella + close: + other: Chiudi + reopen: + other: Riapri + forbidden_error: + other: Vietato. + pin: + other: Fissa sul profilo + hide: + other: Rimuovi dall'elenco + unpin: + other: Stacca dal profilo + show: + other: Aggiungi all'elenco + invite_someone_to_answer: + other: Modifica + undelete: + other: Ripristina + merge: + other: Merge + role: name: - other: "altro" + user: + other: Utente + admin: + other: Amministratore + moderator: + other: Moderatore description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." - -question: - close: - duplicate: - name: - other: "spam" + user: + other: Predefinito senza alcun accesso speciale. + admin: + other: Avere il pieno potere di accedere al sito. + moderator: + other: Ha accesso a tutti i post tranne le impostazioni di amministratore. + privilege: + level_1: description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: - name: - other: "motivo legato alla community" + other: 'Livello 1 (per team o gruppi privati: richiesta reputazione limitata)' + level_2: description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: - name: - other: "richiede maggiori dettagli o chiarezza" + other: 'Livello 2 (per startup community: richiesta reputazione scarsa)' + level_3: description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." - other: - name: - other: "altro" + other: 'Livello 3 (per community riconosciute: richiesta reputazione alta)' + level_custom: description: - other: "Questo articolo richiede un'altro motivo non listato sopra." + other: Livello personalizzato + rank_question_add_label: + other: Fai una domanda + rank_answer_add_label: + other: Scrivi una risposta + rank_comment_add_label: + other: Scrivi un commento + rank_report_add_label: + other: Segnala + rank_comment_vote_up_label: + other: Approva commento + rank_link_url_limit_label: + other: Pubblica più di 2 link alla volta + rank_question_vote_up_label: + other: Approva domanda + rank_answer_vote_up_label: + other: Approva risposta + rank_question_vote_down_label: + other: Disapprova domanda + rank_answer_vote_down_label: + other: Disapprova risposta + rank_invite_someone_to_answer_label: + other: Invita qualcuno a rispondere + rank_tag_add_label: + other: Crea un nuovo tag + rank_tag_edit_label: + other: Modifica descrizione tag (necessità di revisione) + rank_question_edit_label: + other: Modifica la domanda di altri (necessità di revisione) + rank_answer_edit_label: + other: Modifica la risposta di altri (necessità di revisione) + rank_question_edit_without_review_label: + other: Modifica la domanda di altri senza bisogno di revisione + rank_answer_edit_without_review_label: + other: Modifica la risposta di altri senza bisogno di revisione + rank_question_audit_label: + other: Rivedi modifiche alla domanda + rank_answer_audit_label: + other: Rivedi modifiche alla risposta + rank_tag_audit_label: + other: Esamina le modifiche ai tag + rank_tag_edit_without_review_label: + other: Modifica la descrizione del tag senza necessità di revisione + rank_tag_synonym_label: + other: Gestisci sinonimi dei tag + email: + other: E-mail + e_mail: + other: E-mail + password: + other: Chiave di accesso + pass: + other: Password + old_pass: + other: Current password + original_text: + other: Questo post + email_or_password_wrong_error: + other: Email o password errati. + error: + common: + invalid_url: + other: URL non valido. + status_invalid: + other: Status non valido. + password: + space_invalid: + other: La password non può contenere spazi. + admin: + cannot_update_their_password: + other: Non è possibile modificare la password. + cannot_edit_their_profile: + other: Non è possibile modificare il profilo. + cannot_modify_self_status: + other: Non è possibile modificare il tuo status. + email_or_password_wrong: + other: Email o password errati. + answer: + not_found: + other: Risposta non trovata. + cannot_deleted: + other: Permesso per cancellare mancante. + cannot_update: + other: Nessun permesso per l'aggiornamento. + question_closed_cannot_add: + other: Le domande sono chiuse e non possono essere aggiunte. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Non si hanno di privilegi sufficienti per modificare il commento. + not_found: + other: Commento non trovato. + cannot_edit_after_deadline: + other: Il tempo per editare è scaduto. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email già esistente. + need_to_be_verified: + other: L'email deve essere verificata. + verify_url_expired: + other: L'url di verifica email è scaduto, si prega di reinviare l'email. + illegal_email_domain_error: + other: L'email non è consentita da quel dominio di posta elettronica. Si prega di usarne un altro. + lang: + not_found: + other: File lingua non trovato. + object: + captcha_verification_failed: + other: Captcha errato. + disallow_follow: + other: Non sei autorizzato a seguire + disallow_vote: + other: non sei autorizzato a votare + disallow_vote_your_self: + other: Non puoi votare un tuo post! + not_found: + other: oggetto non trovato + verification_failed: + other: verifica fallita + email_or_password_incorrect: + other: email o password incorretti + old_password_verification_failed: + other: la verifica della vecchia password è fallita + new_password_same_as_previous_setting: + other: La nuova password è identica alla precedente + already_deleted: + other: Questo post è stato eliminato. + meta: + object_not_found: + other: Meta oggetto non trovato + question: + already_deleted: + other: Questo post è stato eliminato. + under_review: + other: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato. + not_found: + other: domanda non trovata + cannot_deleted: + other: Permesso per cancellare mancante. + cannot_close: + other: Nessun permesso per chiudere. + cannot_update: + other: Nessun permesso per l'aggiornamento. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Il rango di reputazione non soddisfa le condizioni. + vote_fail_to_meet_the_condition: + other: Grazie per il feedback. Hai bisogno di almeno una reputazione {{.Rank}} per votare. + no_enough_rank_to_operate: + other: Hai bisogno di almeno una reputazione {{.Rank}} per fare questo. + report: + handle_failed: + other: Gestione del report fallita + not_found: + other: Report non trovato + tag: + already_exist: + other: Tag già esistente. + not_found: + other: Etichetta non trovata + recommend_tag_not_found: + other: Il tag consigliato non esiste. + recommend_tag_enter: + other: Inserisci almeno un tag. + not_contain_synonym_tags: + other: Non deve contenere tag sinonimi. + cannot_update: + other: Nessun permesso per l'aggiornamento. + is_used_cannot_delete: + other: Non è possibile eliminare un tag in uso. + cannot_set_synonym_as_itself: + other: Non puoi impostare il sinonimo del tag corrente come se stesso. + smtp: + config_from_name_cannot_be_email: + other: Il mittente non può essere un indirizzo email. + theme: + not_found: + other: tema non trovato + revision: + review_underway: + other: Non è possibile modificare al momento, c'è una versione nella coda di revisione. + no_permission: + other: Non è permessa la revisione. + user: + external_login_missing_user_id: + other: La piattaforma di terze parti non fornisce un utente ID unico, quindi non è possibile effettuare il login. Si prega di contattare l'amministratore del sito web. + external_login_unbinding_forbidden: + other: Per favore imposta una password di login per il tuo account prima di rimuovere questo accesso. + email_or_password_wrong: + other: + other: Email o password errati + not_found: + other: utente non trovato + suspended: + other: utente sospeso + username_invalid: + other: utente non valido + username_duplicate: + other: Nome utente già in uso + set_avatar: + other: Inserimento dell'Avatar non riuscito. + cannot_update_your_role: + other: Non puoi modificare il tuo ruolo. + not_allowed_registration: + other: Attualmente il sito non è aperto per la registrazione. + not_allowed_login_via_password: + other: Attualmente non è consentito accedere al sito tramite password. + access_denied: + other: Accesso negato + page_access_denied: + other: Non hai accesso a questa pagina. + add_bulk_users_format_error: + other: "Errore {{.Field}} formato vicino a '{{.Content}}' alla riga {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Il numero di utenti che aggiungi contemporaneamente dovrebbe essere compreso tra 1 e {{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Configurazione lettura fallita + database: + connection_failed: + other: Connessione al database fallita + create_table_failed: + other: Creazione tabella non riuscita + install: + create_config_failed: + other: Impossibile creare il file config.yaml. + upload: + unsupported_file_format: + other: Formato file non supportato. + site_info: + config_not_found: + other: Configurazione del sito non trovata. + badge: + object_not_found: + other: Oggetto badge non trovato + reason: + spam: + name: + other: Spam + desc: + other: "Questo post è una pubblicità o spam. Non è utile o rilevante per l'argomento attuale.\n" + rude_or_abusive: + name: + other: scortese o offensivo + desc: + other: "Una persona ragionevole troverebbe questo contenuto inappropriato per un discorso rispettoso." + a_duplicate: + name: + other: duplicato + desc: + other: Questa domanda è già stata posta e ha già una risposta. + placeholder: + other: Inserisci il link alla domanda esistente + not_a_answer: + name: + other: Non è una risposta + desc: + other: "Questo è stato pubblicato come una risposta, ma non tenta di rispondere alla domanda. Dovrebbe forse essere una modifica, un commento, un'altra domanda,o cancellato del tutto." + no_longer_needed: + name: + other: Non più necessario + desc: + other: Questo commento è obsoleto, informale o non rilevante per questo post. + something: + name: + other: Qualcos'altro + desc: + other: Questo post richiede l'attenzione dello staff per un altro motivo non elencato sopra. + placeholder: + other: Facci sapere nello specifico cosa ti preoccupa + community_specific: + name: + other: Un motivo legato alla community + desc: + other: Questa domanda non soddisfa le linee guida della community. + not_clarity: + name: + other: Richiede maggiori dettagli o chiarezza + desc: + other: Questa domanda include più domande in una. Dovrebbe concentrarsi su un unico problema. + looks_ok: + name: + other: sembra OK + desc: + other: Questo post è corretto così com'è e non è di bassa qualità. + needs_edit: + name: + other: Necessita di modifiche che ho effettuato + desc: + other: Migliora e correggi tu stesso i problemi di questo post. + needs_close: + name: + other: necessita di chiusura + desc: + other: A una domanda chiusa non è possibile rispondere, ma è comunque possibile modificare, votare e commentare. + needs_delete: + name: + other: Necessario eliminare + desc: + other: Questo post verrà eliminato. + question: + close: + duplicate: + name: + other: posta indesiderata + desc: + other: Questa domanda è già stata posta e ha già una risposta. + guideline: + name: + other: motivo legato alla community + desc: + other: Questa domanda non soddisfa le linee guida della comunità. + multiple: + name: + other: richiede maggiori dettagli o chiarezza + desc: + other: Questa domanda attualmente include più domande in uno. Dovrebbe concentrarsi su un solo problema. + other: + name: + other: altro + desc: + other: Questo articolo richiede un'altro motivo non listato sopra. + operation_type: + asked: + other: chiesto + answered: + other: Risposto + modified: + other: Modificato + deleted_title: + other: "\nDomanda cancellata" + questions_title: + other: Domande + tag: + tags_title: + other: Tags + no_description: + other: Il tag non ha descrizioni. + notification: + action: + update_question: + other: domanda aggiornata + answer_the_question: + other: domanda risposta + update_answer: + other: risposta aggiornata + accept_answer: + other: risposta accettata + comment_question: + other: domanda commentata + comment_answer: + other: risposta commentata + reply_to_you: + other: hai ricevuto risposta + mention_you: + other: sei stato menzionato + your_question_is_closed: + other: la tua domanda è stata chiusa + your_question_was_deleted: + other: la tua domanda è stata rimossa + your_answer_was_deleted: + other: la tua risposta è stata rimossa + your_comment_was_deleted: + other: il tuo commento è stato rimosso + up_voted_question: + other: domanda approvata + down_voted_question: + other: domanda scartata + up_voted_answer: + other: risposta approvata + down_voted_answer: + other: risposta sfavorevole + up_voted_comment: + other: commento approvato + invited_you_to_answer: + other: sei invitato a rispondere + earned_badge: + other: Hai ottenuto il badge "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Conferma il tuo nuovo indirizzo email" + body: + other: "Conferma il tuo nuovo indirizzo email per {{.SiteName}} cliccando sul seguente link:
\n{{.ChangeEmailUrl}}

\n\nSe non hai richiesto questa modifica, ignora questa email.

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} ha risposto alla tua domanda" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} ti ha invitato a rispondere" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Penso che tu possa sapere la risposta.

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} ha commentato il tuo post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVisualizza su {{.SiteName}}

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata.

\n\nCancellazione" + new_question: + title: + other: "[{{.SiteName}}] Nuova domanda: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Reimpostazione della password" + body: + other: "Qualcuno ha chiesto di reimpostare la tua password su {{.SiteName}}.

\n\nSe non sei tu, puoi tranquillamente ignorare questa email.

\n\nClicca sul seguente link per scegliere una nuova password:
\n{{.PassResetUrl}}\n

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." + register: + title: + other: "[{{.SiteName}}] Conferma il tuo nuovo account" + body: + other: "Benvenuto in {{.SiteName}}!

\n\nClicca il seguente link per confermare e attivare il tuo nuovo account:
\n{{.RegisterUrl}}

\n\nSe il link di cui sopra non è cliccabile, prova a copiarlo e incollarlo nella barra degli indirizzi del tuo browser web.\n

\n\n--
\nNota: Si tratta di un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." + test: + title: + other: "[{{.SiteName}}] Email di prova" + body: + other: "Questa è una email di prova.\n

\n\n--
\nNota: Questa è un'email di sistema automatica, non rispondere a questo messaggio perché la tua risposta non verrà visualizzata." + action_activity_type: + upvote: + other: voto a favore + upvoted: + other: voto a favore + downvote: + other: voto negativo + downvoted: + other: votato negativamente + accept: + other: Accetta + accepted: + other: Accettato + edit: + other: modifica + review: + queued_post: + other: Post in coda + flagged_post: + other: Post contrassegnato + suggested_post_edit: + other: Modifiche suggerite + reaction: + tooltip: + other: "{{ .Names }} e {{ .Count }} più..." + badge: + default_badges: + autobiographer: + name: + other: Autobiografo + desc: + other: Informazioni sul profilo completate. + certified: + name: + other: Certificato + desc: + other: "\nCompletato il nostro nuovo tutorial per l'utente." + editor: + name: + other: Editor + desc: + other: Prima modifica al post. + first_flag: + name: + other: Primo Contrassegno + desc: + other: Primo contrassegno di un post. + first_upvote: + name: + other: Primo Mi Piace + desc: + other: Primo Mi Piace a un post + first_link: + name: + other: Primo Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: Prima Reazione + desc: + other: Prima reazione al post. + first_share: + name: + other: Prima Condivisione + desc: + other: Prima condivisione a un post. + scholar: + name: + other: Studioso + desc: + other: Ha posto una domanda e ha accettato una risposta + commentator: + name: + other: Commentatore + desc: + other: Lascia 5 commenti. + new_user_of_the_month: + name: + other: Nuovo Utente del Mese + desc: + other: Contributi straordinari nel primo mese. + read_guidelines: + name: + other: Leggi le Linee Guida + desc: + other: Leggi le [linee guida della community]. + reader: + name: + other: Lettore + desc: + other: Leggi ogni risposta in un argomento con più di 10 risposte. + welcome: + name: + other: Benvenuto + desc: + other: Ricevuto un voto positivo. + nice_share: + name: + other: Condivisione positiva. + desc: + other: Ha condiviso un post con 25 visitatori unici. + good_share: + name: + other: Condivisione positiva. + desc: + other: Condiviso un post con 300 visitatori unici. + great_share: + name: + other: Grande Condivisione + desc: + other: Condiviso un post con 1000 visitatori unici. + out_of_love: + name: + other: Fuori Amore + desc: + other: Usato 50 voti in su in un giorno. + higher_love: + name: + other: Amore Superiore + desc: + other: Usato 50 voti in su in un giorno 5 volte. + crazy_in_love: + name: + other: Pazzo innamorato + desc: + other: Utilizzato 50 voti in su in un giorno 20 volte. + promoter: + name: + other: Promotore + desc: + other: Invitato un utente. + campaigner: + name: + other: Campagnia + desc: + other: Invitato a 3 utenti di base. + champion: + name: + other: Campione + desc: + other: Invitati 5 membri. + thank_you: + name: + other: Grazie + desc: + other: Il post ha ricevuto 20 voti positivi e 10 voti positivi. + gives_back: + name: + other: Feedback + desc: + other: Post con 100 voti positivi e 100 voti positivi espressi. + empathetic: + name: + other: Empatico + desc: + other: Ha 500 posti votati e ha rinunciato a 1000 voti. + enthusiast: + name: + other: Entusiasta + desc: + other: Visitato 10 giorni consecutivi. + aficionado: + name: + other: Aficionado + desc: + other: Visitato 100 giorni consecutivi. + devotee: + name: + other: Devotee + desc: + other: Visitato 365 giorni consecutivi. + anniversary: + name: + other: Anniversario + desc: + other: Membro attivo per un anno, pubblicato almeno una volta. + appreciated: + name: + other: Apprezzato + desc: + other: Ricevuto 1 voto su 20 posti. + respected: + name: + other: Rispettati + desc: + other: Ricevuto 2 voto su 100 posti. + admired: + name: + other: Ammirato + desc: + other: Ricevuto 5 voto su 300 posti. + solved: + name: + other: Risolto + desc: + other: Avere una risposta accettata. + guidance_counsellor: + name: + other: Consulente Di Orientamento + desc: + other: Si accettano 10 risposte. + know_it_all: + name: + other: Sa tutto + desc: + other: Si accettano 50 risposte. + solution_institution: + name: + other: Istituzione Di Soluzione + desc: + other: Si accettano 150 risposte. + nice_answer: + name: + other: Bella risposta + desc: + other: Punteggio domande pari o superiore a 10. + good_answer: + name: + other: Buona risposta + desc: + other: Punteggio domande pari o superiore a 25. + great_answer: + name: + other: Risposta molto buona + desc: + other: Punteggio domande pari o superiore a 50. + nice_question: + name: + other: Bella domanda + desc: + other: Punteggio domande pari o superiore a 10. + good_question: + name: + other: Buona domanda + desc: + other: Punteggio della domanda di 25 o più + great_question: + name: + other: Ottima domanda + desc: + other: Punteggio domande pari o superiore a 50. + popular_question: + name: + other: Domanda popolare + desc: + other: "Domanda con 500 visualizzazioni\n" + notable_question: + name: + other: Domanda notevole + desc: + other: Domanda con 1.000 visualizzazioni. + famous_question: + name: + other: Domanda celebre + desc: + other: "Domanda con 5.000 visualizzazioni.\n." + popular_link: + name: + other: Link Popolare + desc: + other: Pubblicato un link esterno con 50 clic. + hot_link: + name: + other: Link popolare + desc: + other: Pubblicato un link esterno con 300 clic. + famous_link: + name: + other: Link celebre + desc: + other: Pubblicato un link esterno con 100 clic. + default_badge_groups: + getting_started: + name: + other: Primi passi + community: + name: + other: Community + posting: + name: + other: Pubblicazione in corso +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Come formattare + desc: >- + + pagination: + prev: Prec + next: Successivo + page_title: + question: Domanda + questions: Domande + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Crea tag + edit_tag: Modifica Tag + ask_a_question: Create Question + edit_question: Modifica Domanda + edit_answer: Modifica risposta + search: Cerca + posts_containing: Post contenenti + settings: Impostazioni + notifications: Notifiche + login: Accedi + sign_up: Registrati + account_recovery: Recupero dell'account + account_activation: Attivazione account + confirm_email: Conferma Email + account_suspended: Account sospeso + admin: Amministratore + change_email: Modifica la tua email + install: Installazione di Answer + upgrade: Upgrade di Answer + maintenance: Manutenzione del sito + users: Utenti + oauth_callback: Elaborazione in corso + http_404: Errore HTTP 404 + http_50X: Errore HTTP 500 + http_403: Errore HTTP 403 + logout: Disconnetti + notifications: + title: Notifiche + inbox: Posta in arrivo + achievement: Risultati + new_alerts: Nuovi avvisi + all_read: Segna tutto come letto + show_more: Mostra di più + someone: Qualcuno + inbox_type: + all: Tutti + posts: Messaggi + invites: Inviti + votes: Voti + answer: Risposta + question: Domanda + badge_award: Distintivo + suspended: + title: Il tuo account è stato sospeso + until_time: "Il tuo account è stato sospeso fino a {{ time }}." + forever: Questo utente è stato sospeso per sempre. + end: Non soddisfi le linee guida della community. + contact_us: Contattaci + editor: + blockquote: + text: Citazione + bold: + text: Forte + chart: + text: Grafico + flow_chart: Diagramma di flusso + sequence_diagram: Diagramma di sequenza + class_diagram: Diagramma di classe + state_diagram: Diagramma di stato + entity_relationship_diagram: Diagramma Entità-Relazione + user_defined_diagram: "\nDiagramma definito dall'utente" + gantt_chart: Diagramma di Gantt + pie_chart: Grafico a torta + code: + text: Esempio di codice + add_code: Aggiungi un esempio di codice + form: + fields: + code: + label: Codice + msg: + empty: Il codice non può essere vuoto + language: + label: Lingua + placeholder: Rilevamento automatico + btn_cancel: Cancella + btn_confirm: Aggiungi + formula: + text: Formula + options: + inline: Formula nella linea + block: "\nFormula di blocco" + heading: + text: Intestazione + options: + h1: Intestazione 1 + h2: Intestazione 2 + h3: Intestazione 3 + h4: Intestazione 4 + h5: Intestazione 5 + h6: Intestazione 6 + help: + text: Aiuto + hr: + text: Riga orizzontale + image: + text: Immagine + add_image: Aggiungi immagine + tab_image: Carica immagine + form_image: + fields: + file: + label: File d'immagine + btn: Scegli immagine + msg: + empty: Il file non può essere vuoto. + only_image: Sono ammesse solo le immagini + max_size: La dimensione del file non può superare {{size}} MB. + desc: + label: Descrizione + tab_url: Url dell'Immagine + form_url: + fields: + url: + label: URL dell'immagine + msg: + empty: L'URL dell'immagine non può essere vuoto. + name: + label: Descrizione + btn_cancel: Cancella + btn_confirm: Aggiungi + uploading: Caricamento in corso... + indent: + text: Indenta + outdent: + text: Deindenta + italic: + text: Corsivo + link: + text: Collegamento ipertestuale + add_link: Aggiungi collegamento + form: + fields: + url: + label: URL + msg: + empty: L'URL non può essere vuoto + name: + label: Descrizione + btn_cancel: Cancella + btn_confirm: Aggiungi + ordered_list: + text: Elenco numerato + unordered_list: + text: Elenco puntato + table: + text: Tabella + heading: Intestazione + cell: Cella + file: + text: Allega file + not_supported: "Non supportare quel tipo di file. Riprovare con {{file_type}}." + max_size: "Allega la dimensione dei file non può superare {{size}} MB." + close_modal: + title: Sto chiudendo questo post come... + btn_cancel: Cancella + btn_submit: Invia + remark: + empty: Non può essere vuoto. + msg: + empty: Per favore seleziona un motivo + report_modal: + flag_title: Sto segnalando questo post per riportarlo come... + close_title: Sto chiudendo questo post come... + review_question_title: Rivedi la domanda + review_answer_title: Rivedi la risposta + review_comment_title: Rivedi il commento + btn_cancel: Cancella + btn_submit: Invia + remark: + empty: Non può essere vuoto. + msg: + empty: Per favore seleziona un motivo + not_a_url: Formato URL errato. + url_not_match: L'origine dell'URL non corrisponde al sito web corrente. + tag_modal: + title: Crea un nuovo tag + form: + fields: + display_name: + label: Nome da visualizzare + msg: + empty: Il nome utente non può essere vuoto. + range: Nome utente fino a 35 caratteri. + slug_name: + label: Slug dell'URL + desc: URL slug fino a 35 caratteri. + msg: + empty: Lo slug dell'URL non può essere vuoto. + range: Slug dell'URL fino a 35 caratteri. + character: Lo slug dell'URL contiene un set di caratteri non consentiti. + desc: + label: Descrizione + revision: + label: Revisione + edit_summary: + label: Modifica il riepilogo + placeholder: >- + Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata) + btn_cancel: Cancella + btn_submit: Invia + btn_post: Pubblica un nuovo tag + tag_info: + created_at: Creato + edited_at: Modificato + history: Cronologia + synonyms: + title: Sinonimi + text: I seguenti tag verranno rimappati a + empty: Nessun sinonimo trovato. + btn_add: Aggiungi un sinonimo + btn_edit: Modifica + btn_save: Salva + synonyms_text: I seguenti tag verranno rimappati a + delete: + title: Elimina questo tag + tip_with_posts: >- +

Non è consentita l'eliminazione di tag con post.

Rimuovi prima questo tag dai post.

+ tip_with_synonyms: >- +

Non è consentita l'eliminazione di tag con sinonimi.

Per favore, rimuovi prima i sinonimi da questo tag.

+ tip: Sei sicuro di voler cancellare? + close: Chiudi + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Modifica Tag + default_reason: Modifica tag + default_first_reason: Aggiungi tag + btn_save_edits: Salva modifiche + btn_cancel: Cancella + dates: + long_date: Mese, giorno + long_date_with_year: "Giorno, Mese, Anno" + long_date_with_time: "MMM D, AAAA [at] HH:mm" + now: ora + x_seconds_ago: "{{count}}s fa" + x_minutes_ago: "{{count}}m fa" + x_hours_ago: "{{count}}h fa" + hour: ora + day: giorno + hours: ore + days: giorni + month: month + months: months + year: year + reaction: + heart: cuore + smile: sorriso + frown: disapprovare + btn_label: aggiungere o rimuovere le reazioni + undo_emoji: annulla reazione {{ emoji }} + react_emoji: reagire con {{ emoji }} + unreact_emoji: non reagire con {{ emoji }} + comment: + btn_add_comment: Aggiungi un commento + reply_to: Rispondi a + btn_reply: Rispondi + btn_edit: Modifica + btn_delete: Cancella + btn_flag: Segnala + btn_save_edits: Salva modifiche + btn_cancel: Cancella + show_more: "{{count}} altri commenti" + tip_question: >- + Usa i commenti per chiedere maggiori informazioni o per suggerire miglioramenti. Evita di rispondere alle domande nei commenti. + tip_answer: >- + Utilizza i commenti per rispondere ad altri utenti o avvisarli delle modifiche. Se stai aggiungendo nuove informazioni, modifica il tuo post invece di commentare. + tip_vote: Aggiunge qualcosa di utile al post + edit_answer: + title: Modifica risposta + default_reason: Modifica risposta + default_first_reason: Aggiungi risposta + form: + fields: + revision: + label: Revisione + answer: + label: Risposta + feedback: + characters: Il testo deve contenere almeno 6 caratteri. + edit_summary: + label: Modifica il riepilogo + placeholder: >- + Spiega brevemente le tue modifiche (ortografia rifinita, grammatica corretta, formattazione migliorata) + btn_save_edits: Salva modifiche + btn_cancel: Annulla + tags: + title: Tag + sort_buttons: + popular: Popolari + name: Nome + newest: Più recente + button_follow: Segui + button_following: Segui già + tag_label: domande + search_placeholder: Filtra per nome del tag + no_desc: Il tag non ha descrizioni. + more: Altro + wiki: Wiki + ask: + title: Create Question + edit_title: Modifica Domanda + default_reason: Modifica domanda + default_first_reason: Create question + similar_questions: Domande simili + form: + fields: + revision: + label: Revisione + title: + label: Titolo + placeholder: What's your topic? Be specific. + msg: + empty: Il titolo non può essere vuoto. + range: Il titolo non può superare i 150 caratteri + body: + label: Contenuto + msg: + empty: Il corpo del testo non può essere vuoto. + tags: + label: Tags + msg: + empty: I tag non possono essere vuoti. + answer: + label: Risposta + msg: + empty: La risposta non può essere vuota. + edit_summary: + label: Modifica riepilogo + placeholder: >- + Spiega brevemente le tue modifiche (ortografia corretta, grammatica corretta, formattazione migliorata) + btn_post_question: Posta la tua domanda + btn_save_edits: Salva modifiche + answer_question: Rispondi alla tua domanda + post_question&answer: Posta la tua domanda e risposta + tag_selector: + add_btn: Aggiungi tag + create_btn: Crea un nuovo tag + search_tag: Cerca tag + hint: "Describe what your content is about, at least one tag is required." + no_result: Nessun tag corrispondente + tag_required_text: Tag richiesto (almeno uno) + header: + nav: + question: Domande + tag: Tags + user: Utenti + badges: Badges + profile: Profilo + setting: Impostazioni + logout: Disconnetti + admin: Amministratore + review: Revisione + bookmark: Segnalibri + moderation: Moderazione + search: + placeholder: Cerca + footer: + build_on: >- + Basato su <1> Apache Answer , il software open source che alimenta le comunità di domande e risposte.
Fatto con amore © {{cc}}. + upload_img: + name: Modifica + loading: caricamento in corso... + pic_auth_code: + title: Captcha + placeholder: Digita il testo sopra + msg: + empty: Il Captcha non può essere vuoto. + inactive: + first: >- + Hai quasi finito! Abbiamo inviato un'e-mail di attivazione a {{mail}}. Segui le istruzioni contenute nella mail per attivare il tuo account. + info: "Se non arriva, controlla la cartella spam." + another: >- + Ti abbiamo inviato un'altra email di attivazione all'indirizzo {{mail}}. Potrebbero volerci alcuni minuti prima che arrivi; assicurati di controllare la cartella spam. + btn_name: Reinvia l'e-mail di attivazione + change_btn_name: Modifica e-mail + msg: + empty: Non può essere vuoto. + resend_email: + url_label: Sei sicuro di voler inviare nuovamente l'e-mail di attivazione? + url_text: Puoi anche fornire all'utente il link di attivazione riportato sopra. + login: + login_to_continue: Accedi per continuare + info_sign: Non hai un account? <1>Iscriviti + info_login: Hai già un account? <1>Accedi + agreements: Registrandoti, accetti l'<1>informativa sulla privacy e i <3>termini del servizio. + forgot_pass: Password dimenticata? + name: + label: Nome + msg: + empty: Il nome non può essere vuoto. + range: Il nome deve essere di lunghezza compresa tra 2 e 30 caratteri. + character: 'È necessario utilizzare il set di caratteri "a-z", "0-9", " - . _"' + email: + label: E-mail + msg: + empty: L'email non può essere vuota. + password: + label: Password + msg: + empty: La password non può essere vuota. + different: Le password inserite su entrambi i lati non corrispondono + account_forgot: + page_title: Password dimenticata? + btn_name: Inviami email di recupero + send_success: >- + Se un account corrisponde a {{mail}}, a breve dovresti ricevere un'e-mail con le istruzioni su come reimpostare la password. + email: + label: E-mail + msg: + empty: Il campo email non può essere vuoto. + change_email: + btn_cancel: Cancella + btn_update: Aggiorna indirizzo email + send_success: >- + Se un account corrisponde a {{mail}}, a breve dovresti ricevere un'email con le istruzioni su come reimpostare la password. + email: + label: Nuova email + msg: + empty: L'email non può essere vuota. + oauth: + connect: Connettiti con {{ auth_name }} + remove: Rimuovi {{ auth_name }} + oauth_bind_email: + subtitle: Aggiungi un'email di recupero al tuo account. + btn_update: Aggiorna l'indirizzo email + email: + label: E-mail + msg: + empty: L'email non può essere vuota. + modal_title: Email già esistente + modal_content: Questo indirizzo email è già registrato. Sei sicuro che vuoi connetterti all'account esistente? + modal_cancel: Cambia email + modal_confirm: Connettiti all'account esistente + password_reset: + page_title: Reimposta la password + btn_name: Reimposta la mia password + reset_success: >- + Hai cambiato con successo la tua password; sarai reindirizzato alla pagina di accesso. + link_invalid: >- + Siamo spiacenti, questo link di reset della password non è più valido. Forse la password è già stata reimpostata? + to_login: Continua per effettuare il login + password: + label: Password + msg: + empty: La password non può essere vuota + length: La lunghezza deve essere compresa tra 8 e 32 + different: Le password inserite non corrispondono + password_confirm: + label: Conferma la nuova password + settings: + page_title: Impostazioni + goto_modify: Vai alle modifiche + nav: + profile: Profilo + notification: Notifiche + account: Profilo + interface: Interfaccia + profile: + heading: Profilo + btn_name: Salva + display_name: + label: Visualizza nome + msg: Il nome utente non può essere vuoto. + msg_range: Display name must be 2-30 characters in length. + username: + label: Nome utente + caption: Gli altri utenti possono menzionarti con @{{username}}. + msg: Il nome utente non può essere vuoto. + msg_range: Username must be 2-30 characters in length. + character: 'È necessario utilizzare il set di caratteri "a-z", "0-9", " - . _"' + avatar: + label: Immagine del profilo + gravatar: Gravatar + gravatar_text: Puoi cambiare l'immagine + custom: Personalizzato + custom_text: Puoi caricare la tua immagine. + default: Sistema + msg: Per favore carica un avatar + bio: + label: Chi sono + website: + label: Sito web + placeholder: "https://esempio.com" + msg: Formato non corretto del sito web + location: + label: Luogo + placeholder: "Città, Paese" + notification: + heading: Notifiche email + turn_on: Accendi + inbox: + label: Notifiche in arrivo + description: Risposte alle tue domande, commenti, inviti e altro ancora. + all_new_question: + label: Tutte le nuove domande + description: Ricevi una notifica per tutte le nuove domande. Fino a 50 domande a settimana. + all_new_question_for_following_tags: + label: Tutte le nuove domande per i seguenti tag + description: Ricevi una notifica delle nuove domande per i seguenti tag. + account: + heading: Profilo + change_email_btn: Modifica e-mail + change_pass_btn: Modifica password + change_email_info: >- + Abbiamo inviato una mail a quell'indirizzo. Si prega di seguire le istruzioni di conferma. + email: + label: E-mail + new_email: + label: Nuova mail + msg: La nuova email non può essere vuota. + pass: + label: Password attuale + msg: La password non può essere vuota + password_title: Password + current_pass: + label: Password attuale + msg: + empty: La password attuale non può essere vuota. + length: La lunghezza deve essere compresa tra 8 e 32. + different: Le due password inserite non corrispondono. + new_pass: + label: Nuova password + pass_confirm: + label: Conferma la nuova password + interface: + heading: Interfaccia + lang: + label: Lingua dell'interfaccia + text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina. + my_logins: + title: I miei login + label: Accedi o registrati su questo sito utilizzando questi account. + modal_title: Rimuovi login + modal_content: Sei sicuro di voler rimuovere questo login dal tuo account? + modal_confirm_btn: Rimuovi + remove_success: Rimosso con successo + toast: + update: Aggiornamento riuscito + update_password: Password modificata con successo. + flag_success: Grazie per la segnalazione. + forbidden_operate_self: Vietato operare su se stessi. + review: Le tue modifiche verranno visualizzata dopo la revisione. + sent_success: Inviato correttamente + related_question: + title: Related + answers: risposte + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Persone Interpellate + desc: Seleziona le persone che pensi potrebbero conoscere la risposta. + invite: Invita a rispondere + add: Aggiungi contatti + search: Cerca persone + question_detail: + action: Azione + Asked: Chiesto + asked: chiesto + update: Modificato + edit: modificato + commented: commentato + Views: Visualizzati + Follow: Segui + Following: Segui già + follow_tip: Segui questa domanda per ricevere notifiche + answered: Risposto + closed_in: Chiuso in + show_exist: Mostra domanda esistente. + useful: Utile + question_useful: È utile e chiaro + question_un_useful: Non è chiaro né utile + question_bookmark: Aggiungi questa domanda ai segnalibri + answer_useful: È utile + answer_un_useful: Non è utile + answers: + title: Risposte + score: Punteggio + newest: Più recenti + oldest: Meno recente + btn_accept: Accetta + btn_accepted: Accettato + write_answer: + title: La tua risposta + edit_answer: Modifica la mia risposta attuale + btn_name: Pubblica la tua risposta + add_another_answer: Aggiungi un'altra risposta + confirm_title: Continua a rispondere + continue: Continua + confirm_info: >- +

Sei sicuro di voler aggiungere un'altra risposta?

In alternativa, puoi usare il link di modifica per perfezionare e migliorare la tua risposta esistente.

+ empty: La risposta non può essere vuota. + characters: Il contenuto deve avere una lunghezza di almeno 6 caratteri. + tips: + header_1: Grazie per la risposta + li1_1: Assicurati di rispondere alla domanda. Fornisci dettagli e condividi la tua ricerca. + li1_2: Effettua il backup di qualsiasi dichiarazione fatta con riferimenti o esperienze personali. + header_2: Ma evita... + li2_1: Chiedere aiuto, cercare chiarimenti o rispondere ad altre risposte. + reopen: + confirm_btn: Riapri + title: Riapri questo post + content: Sei sicuro di voler riaprire? + list: + confirm_btn: Listare + title: Lista questo post + content: Sei sicuro di volerlo listare? + unlist: + confirm_btn: Rimuovi dall'elenco + title: Rimuovi questo post + content: Sei sicuro di voler rimuovere dall'elenco? + pin: + title: Fissa questo post in cima al profilo + content: Sei sicuro di voler fissare in blocco? Questo post apparirà in cima a tutte le liste. + confirm_btn: Fissa sul profilo + delete: + title: Cancella questo post + question: >- + Non consigliamo di eliminaredomande con risposte perché ciò priva i futuri lettori di questa conoscenza.

L'eliminazione ripetuta di domande con risposte può comportare il blocco delle domande nel tuo account. Sei sicuro di voler eliminare? + answer_accepted: >- +

Non consigliamo di eliminare la risposta accettata perché così facendo si priva i futuri lettori di questa conoscenza.

La cancellazione ripetuta delle risposte accettate può causare il blocco del tuo account dalla risposta. Sei sicuro di voler eliminare? + other: Sei sicuro di voler eliminare? + tip_answer_deleted: Questa risposta è stata cancellata + undelete_title: Ripristina questo post + undelete_desc: Sei sicuro di voler ripristinare? + btns: + confirm: Conferma + cancel: Cancella + edit: Modifica + save: Salva + delete: Elimina + undelete: Ripristina + list: Aggiungi all'elenco + unlist: Rimuovi dall'elenco + unlisted: Rimosso dall'elenco + login: Accedi + signup: Registrati + logout: Disconnetti + verify: Verifica + create: Create + approve: Approva + reject: Rifiuta + skip: Salta + discard_draft: Elimina bozza + pinned: Fissato in cima + all: Tutti + question: Domanda + answer: Risposta + comment: Commento + refresh: Aggiorna + resend: Rinvia + deactivate: Disattivare + active: Attivo + suspend: Sospendi + unsuspend: Riabilita + close: Chiudi + reopen: Riapri + ok: OK + light: Illuminazione + dark: Scuro + system_setting: Configurazione di sistema + default: Predefinito + reset: Resetta + tag: Tag + post_lowercase: post + filter: 'Filtra' + ignore: Ignora + submit: Invia + normal: Normale + closed: Chiuso + deleted: Eliminato + deleted_permanently: Deleted permanently + pending: In attesa + more: Altro + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Risultati della ricerca + keywords: Parole chiave + options: Opzioni + follow: Segui + following: Segui già + counts: "{{count}} Risultati" + counts_loading: "... Results" + more: Altro + sort_btns: + relevance: Rilevanza + newest: Più recenti + active: Attivo + score: Punteggio + more: Altro + tips: + title: Suggerimenti per ricerca avanzata + tag: "<1>[tag] cerca dentro un tag" + user: "<1>user:username ricerca per autore" + answer: "<1>risposte:0 domande senza risposta" + score: "<1>punteggio:3 messaggi con un punteggio di 3+" + question: "<1>is:question cerca domande" + is_answer: "<1>is:answer cerca risposte" + empty: Non siamo riusciti a trovare nulla.
Prova parole chiave diverse o meno specifiche. + share: + name: Condividi + copy: Copia il link + via: Condividi il post via... + copied: Copiato + facebook: Condividi su Facebook + twitter: Share to X + cannot_vote_for_self: Non puoi votare un tuo post! + modal_confirm: + title: Errore... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Il tuo nuovo account è confermato; sarai reindirizzato alla home page. + link: Continua alla Homepage + oops: Oops! + invalid: Il link che hai usato non è più attivo. + confirm_new_email: La tua email è stata aggiornata. + confirm_new_email_invalid: >- + Siamo spiacenti, questo link di conferma non è più valido. Forse la tua email è già stata modificata? + unsubscribe: + page_title: Annulla l'iscrizione + success_title: Cancellazione effettuata con successo + success_desc: Sei stato rimosso con successo da questa lista e non riceverai ulteriori email + link: Cambia impostazioni + question: + following_tags: Tag seguenti + edit: Modifica + save: Salva + follow_tag_tip: Segui i tag per curare la tua lista di domande. + hot_questions: Domande scottanti + all_questions: Tutte le domande + x_questions: "{{ count }} Domande" + x_answers: "{{count}} risposte" + x_posts: "{{ count }} Posts" + questions: Domande + answers: Risposte + newest: Più recenti + active: Attivo + hot: Caldo + frequent: Frequenti + recommend: Raccomandato + score: Punteggio + unanswered: Senza risposta + modified: Modificato + answered: Risposte + asked: chiesto + closed: Chiuso + follow_a_tag: Segui un tag + more: Altro + personal: + overview: Informazioni Generali + answers: Risposte + answer: Risposta + questions: Domande + question: Domanda + bookmarks: Segnalibri + reputation: Reputazione + comments: Commenti + votes: Voti + badges: Badges + newest: Più recenti + score: Punteggio + edit_profile: Modifica profilo + visited_x_days: "{{ count }} giorni visitati" + viewed: Visualizzati + joined: Iscritto + comma: "," + last_login: Visto + about_me: Chi sono + about_me_empty: "// Ciao, mondo !" + top_answers: Migliori risposte + top_questions: Domande principali + stats: Statistiche + list_empty: Nessun post trovato.
Forse desideri selezionare una scheda diversa? + content_empty: Nessun post trovato. + accepted: Accettato + answered: risposto + asked: chiesto + downvoted: votato negativamente + mod_short: Moderatore + mod_long: Moderatori + x_reputation: reputazione + x_votes: voti ricevuti + x_answers: risposte + x_questions: domande + recent_badges: Badges Recenti + install: + title: Installazione + next: Avanti + done: Fatto + config_yaml_error: Impossibile creare il file config.yaml. + lang: + label: Scegli una lingua + db_type: + label: Motore database + db_username: + label: Nome utente + placeholder: root + msg: Il nome utente non può essere vuoto. + db_password: + label: Password + placeholder: root + msg: La password non può essere vuota. + db_host: + label: Host del database + placeholder: "db:3306" + msg: L'host del database non può essere vuoto. + db_name: + label: Nome database + placeholder: risposta + msg: Il nome del database non può essere vuoto. + db_file: + label: File del database + placeholder: /data/answer.db + msg: Il file del database non può essere vuoto. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Crea config.yaml + label: File config.yaml creato. + desc: >- + Puoi creare manualmente il file <1>config.yaml nella directory <1>/var/wwww/xxx/ e incollarvi il seguente testo. + info: Una volta fatto, fai clic sul pulsante "Avanti". + site_information: Informazioni sul sito + admin_account: Account Amministratore + site_name: + label: Nome del sito + msg: Il nome del sito non può essere vuoto. + msg_max_length: Il nome del sito deve contenere un massimo di 30 caratteri. + site_url: + label: URL del sito + text: L'indirizzo del tuo sito. + msg: + empty: L'URL del sito non può essere vuoto. + incorrect: Formato errato dell'URL del sito. + max_length: L'URL del sito deve contenere un massimo di 512 caratteri. + contact_email: + label: Email di contatto + text: Indirizzo e-mail del contatto chiave responsabile di questo sito. + msg: + empty: L'email del contatto non può essere vuota. + incorrect: Formato errato dell'e-mail di contatto. + login_required: + label: Privato + switch: Login obbligatorio + text: Solo gli utenti registrati possono accedere a questa community. + admin_name: + label: Nome + msg: Il nome non può essere vuoto. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + Avrai bisogno di questa password per accedere. Conservala in un luogo sicuro. + msg: La password non può essere vuota + msg_min_length: La password deve contenere almeno 8 caratteri. + msg_max_length: La password deve contenere un massimo di 32 caratteri. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: E-mail + text: Avrai bisogno di questa email per accedere. + msg: + empty: Il campo email non può essere vuoto. + incorrect: Formato dell'email errato. + ready_title: Il tuo sito è pronto + ready_desc: >- + Se vuoi cambiare più impostazioni, visita la sezione <1>admin che si trova nel menu del sito. + good_luck: "Divertiti e buona fortuna!" + warn_title: Pericolo + warn_desc: >- + Il file <1>config.yaml esiste già. Se vuoi reimpostare uno qualsiasi degli elementi di configurazione in questo file, eliminalo prima. + install_now: Puoi provare a <1>installare ora. + installed: Già installato + installed_desc: >- + Sembra che tu abbia già installato. Per reinstallare, cancella prima le vecchie tabelle del database. + db_failed: Connessione al database fallita + db_failed_desc: >- + Questo significa che le informazioni sul database nel file <1>config.yaml non sono corrette o che non è stato possibile stabilire il contatto con il server del database. Ciò potrebbe significare che il server del database del tuo host è inattivo. + counts: + views: visualizzazioni + votes: Voti + answers: risposte + accepted: Accettato + page_error: + http_error: Errore HTTP {{ code }} + desc_403: Non hai i permessi per accedere a questa pagina. + desc_404: Sfortunatamente, questa pagina non esiste. + desc_50X: Il server ha riscontrato un errore e non è stato possibile completare la richiesta. + back_home: Torna alla home page + page_maintenance: + desc: "Siamo in manutenzione, torneremo presto." + nav_menus: + dashboard: Pannello di controllo + contents: Contenuti + questions: Domande + answers: Risposte + users: Utenti + badges: Badges + flags: Contrassegni + settings: Impostazioni + general: Generale + interface: Interfaccia + smtp: Protocollo di Trasferimento Posta Semplice + branding: Marchio + legal: Legale + write: Scrivi + tos: Termini del servizio + privacy: Privacy + seo: SEO + customize: Personalizza + themes: Temi + login: Accedi + privileges: Privilegi + plugins: Plugin + installed_plugins: Plugin installati + apperance: Appearance + website_welcome: Benvenuto/a su {{site_name}}! + user_center: + login: Accedi + qrcode_login_tip: Si prega di utilizzare {{ agentName }} per scansionare il codice QR e accedere. + login_failed_email_tip: Accesso non riuscito. Consenti a questa app di accedere alle tue informazioni email prima di riprovare. + badges: + modal: + title: Congratulazioni + content: Hai guadagnato un nuovo distintivo. + close: Chiudi + confirm: Visualizza badge + title: Badges + awarded: Premiati + earned_×: Ottenuto ×{{ number }} + ×_awarded: "{{ number }} premiato" + can_earn_multiple: Puoi guadagnare questo più volte. + earned: Ottenuti + admin: + admin_header: + title: Amministratore + dashboard: + title: Pannello di controllo + welcome: Benvenuto ad Admin! + site_statistics: Statistiche del sito + questions: "Domande:" + resolved: "Risolto:" + unanswered: "Senza risposta:" + answers: "Risposte:" + comments: "Commenti:" + votes: "Voti:" + users: "Utenti:" + flags: "Flags" + reviews: "Revisioni" + site_health: Stato del sito + version: "Versione:" + https: "HTTPS:" + upload_folder: "Carica Cartella" + run_mode: "Modalità di esecuzione:" + private: Privato + public: Pubblico + smtp: "Protocollo di Trasferimento Posta Semplice" + timezone: "Fuso orario:" + system_info: Info sistema + go_version: "Versione Go:" + database: "Banca dati:" + database_size: "Dimensioni del database" + storage_used: "Spazio di archiviazione utilizzato:" + uptime: "Tempo di attività:" + links: Collegamenti + plugins: Plugin + github: GitHub + blog: Blog + contact: Contatti + forum: Forum + documents: Documenti + feedback: Feedback + support: Assistenza + review: Revisione + config: Configurazione + update_to: Aggiornato a + latest: Recenti + check_failed: Controllo fallito + "yes": "Sì" + "no": "No" + not_allowed: Non autorizzato + allowed: Consentito + enabled: Abilitato + disabled: Disabilitato + writable: Editabile + not_writable: Non editabile + flags: + title: Contrassegni + pending: In attesa + completed: Completato + flagged: Contrassegnato + flagged_type: Contrassegnato {{ type }} + created: Creato + action: Azione + review: Revisione + user_role_modal: + title: Cambia ruolo utente in... + btn_cancel: Cancella + btn_submit: Invia + new_password_modal: + title: Imposta una nuova password + form: + fields: + password: + label: Password + text: L'utente sarà disconnesso e dovrà effettuare nuovamente il login. + msg: La password deve contenere da 8 a 32 caratteri. + btn_cancel: Cancella + btn_submit: Invia + edit_profile_modal: + title: Modifica profilo + form: + fields: + display_name: + label: Visualizza nome + msg_range: Display name must be 2-30 characters in length. + username: + label: Nome utente + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Indirizzo e-mail non valido. + edit_success: Modificato con successo + btn_cancel: Annulla + btn_submit: Invia + user_modal: + title: Aggiungi un nuovo utente + form: + fields: + users: + label: Aggiungi utenti in blocco + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separare “nome, email, password” con delle virgole. Un utente per riga. + msg: "Inserisci l'email dell'utente, una per riga." + display_name: + label: Nome da visualizzare + msg: Il nome visualizzato deve essere di 2-30 caratteri di lunghezza. + email: + label: E-mail + msg: L'email non è valida. + password: + label: Password + msg: La password deve contenere da 8 a 32 caratteri. + btn_cancel: Cancella + btn_submit: Invia + users: + title: Utenti + name: Nome + email: E-mail + reputation: Reputazione + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Stato + role: Ruolo + action: Azione + change: Modifica + all: Tutti + staff: Personale + more: Altro + inactive: Inattivo + suspended: Sospeso + deleted: Eliminato + normal: Normale + Moderator: Moderatore + Admin: Amministratore + User: Utente + filter: + placeholder: "Filtra per nome, utente:id" + set_new_password: Imposta una nuova password + edit_profile: Modifica profilo + change_status: Modifica lo stato + change_role: Cambia il ruolo + show_logs: Visualizza i log + add_user: Aggiungi utente + deactivate_user: + title: Disattiva utente + content: Un utente inattivo deve riconvalidare la propria email. + delete_user: + title: Rimuovi questo utente + content: Sei sicuro di voler eliminare questo utente? L'operazione è permanente. + remove: Rimuovi il loro contenuto + label: Rimuovi tutte le domande, risposte, commenti, ecc. + text: Non selezionare questa opzione se desideri eliminare solo l'account dell'utente. + suspend_user: + title: Sospendi questo utente + content: Un utente sospeso non può accedere. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Domande + unlisted: Rimosso dall'elenco + post: Posta + votes: Voti + answers: Risposte + created: Creato + status: Stato + action: Azione + change: Modifica + pending: In attesa + filter: + placeholder: "Filtra per titolo, domanda:id" + answers: + page_title: Risposte + post: Post + votes: Voti + created: Creato + status: Stato + action: Azione + change: Cambio + filter: + placeholder: "Filtra per titolo, domanda:id" + general: + page_title: Generale + name: + label: Nome del sito + msg: Il nome del sito non può essere vuoto. + text: "Il nome di questo sito, come usato nel tag del titolo." + site_url: + label: URL del sito + msg: L'url del sito non può essere vuoto. + validate: Inserisci un URL valido. + text: L'indirizzo del tuo sito. + short_desc: + label: Descrizione breve del sito + msg: La descrizione breve del sito non può essere vuota. + text: "Breve descrizione, come utilizzata nel tag del titolo sulla home page." + desc: + label: Descrizione del sito + msg: La descrizione del sito non può essere vuota. + text: "Descrivi questo sito in una frase, come utilizzato nel tag meta description." + contact_email: + label: Email di contatto + msg: L'email del contatto non può essere vuota. + validate: Email di contatto non valida. + text: Indirizzo e-mail del contatto chiave responsabile di questo sito. + check_update: + label: Aggiornamenti Software + text: Controlla automaticamente gli aggiornamenti + interface: + page_title: Interfaccia + language: + label: Lingua dell'interfaccia + msg: La lingua dell'interfaccia non può essere vuota. + text: La lingua dell'interfaccia utente cambierà quando aggiorni la pagina. + time_zone: + label: Fuso orario + msg: Il fuso orario non può essere vuoto. + text: Scegli una città con il tuo stesso fuso orario. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: Protocollo di Trasferimento Posta Semplice + from_email: + label: Dall'email + msg: L'email del contatto non può essere vuota. + text: L'indirizzo email da cui vengono inviate le email. + from_name: + label: Dal nome + msg: "Il nome del mittente non può essere vuoto.\n" + text: Il nome da cui vengono inviate le email. + smtp_host: + label: Host SMTP + msg: L'host SMTP non può essere vuoto. + text: Il tuo server di posta. + encryption: + label: Crittografia + msg: La crittografia non può essere vuota. + text: Per la maggior parte dei server SSL è l'opzione consigliata. + ssl: SSL + tls: TLS + none: Nessuna + smtp_port: + label: Porta SMTP + msg: La porta SMTP deve essere numero 1 ~ 65535. + text: La porta del tuo server di posta. + smtp_username: + label: Nome utente SMTP + msg: Il nome utente SMTP non può essere vuoto. + smtp_password: + label: Password SMTP + msg: La password SMTP non può essere vuota. + test_email_recipient: + label: Verifica destinatari email + text: Fornisci l'indirizzo email che riceverà i test inviati. + msg: Destinatari email di prova non validi + smtp_authentication: + label: "\nAbilita l'autenticazione" + title: Autenticazione SMTP + msg: L'autenticazione SMTP non può essere vuota. + "yes": "Sì" + "no": "No" + branding: + page_title: Marchio + logo: + label: Logo + msg: Il logo non può essere vuoto. + text: L'immagine del logo in alto a sinistra del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56 e proporzioni superiori a 3:1. Se lasciato vuoto, il testo del titolo del sito verrà mostrato. + mobile_logo: + label: Logo per versione mobile + text: "\nIl logo utilizzato nella versione mobile del tuo sito. Utilizza un'immagine rettangolare ampia con un'altezza di 56. Se lasciata vuota, verrà utilizzata l'immagine dell'impostazione \"logo\"." + square_icon: + label: Icona quadrata + msg: L'icona quadrata non può essere vuota. + text: "Immagine utilizzata come base per le icone dei metadata. Idealmente dovrebbe essere più grande di 512x512.\n" + favicon: + label: Favicon + text: Una icona favorita per il tuo sito. Per funzionare correttamente su un CDN deve essere un png. Verrà ridimensionato a 32x32. Se lasciato vuoto, verrà utilizzata l'"icona quadrata". + legal: + page_title: Legale + terms_of_service: + label: Termini del servizio + text: "Puoi aggiungere qui i termini del contenuto del servizio. Se hai già un documento ospitato altrove, fornisci qui l'URL completo." + privacy_policy: + label: Informativa sulla privacy + text: "Puoi aggiungere il contenuto della politica sulla privacy qui. Se hai già un documento ospitato altrove, fornisci l'URL completo qui." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Scrivi + restrict_answer: + title: Risposta a scrivere + label: Ogni utente può scrivere una sola risposta per ogni domanda + text: "Disattiva per consentire agli utenti di scrivere risposte multiple alla stessa domanda, il che potrebbe causare una risposta sfocata." + recommend_tags: + label: Raccomanda tag + text: "I tag consigliati verranno mostrati nell'elenco a discesa per impostazione predefinita." + msg: + contain_reserved: "i tag consigliati non possono contenere tag riservati" + required_tag: + title: Imposta tag necessari + label: Imposta “Raccomanda tag” come tag richiesti + text: "Ogni nuova domanda deve avere almeno un tag raccomandato." + reserved_tags: + label: Tag riservati + text: "I tag riservati possono essere utilizzati solo dal moderatore." + image_size: + label: Dimensione massima dell'immagine (MB) + text: "La dimensione massima del caricamento dell'immagine." + attachment_size: + label: Dimensione massima degli allegati (MB) + text: "Dimensione massima del caricamento dei file allegati." + image_megapixels: + label: Massima immagine megapixel + text: "Numero massimo di megapixel consentiti per un'immagine." + image_extensions: + label: Estensioni di immagini autorizzate + text: "Un elenco di estensioni di file consentite per la visualizzazione dell'immagine, separate da virgole." + attachment_extensions: + label: Estensioni di allegati autorizzate + text: "Una lista di estensioni di file consentite per il caricamento, separate con virgole. ATTENZIONE: Consentire i caricamenti potrebbe causare problemi di sicurezza." + seo: + page_title: SEO + permalink: + label: Permalink + text: Le strutture URL personalizzate possono migliorare l'usabilità e la compatibilità futura dei tuoi link. + robots: + label: robots.txt + text: Questo sovrascriverà definitivamente tutte le impostazioni relative al sito. + themes: + page_title: Temi + themes: + label: Temi + text: Seleziona un tema esistente. + color_scheme: + label: Schema colore + navbar_style: + label: Navbar background style + primary_color: + label: Colore primario + text: Modifica i colori utilizzati dai tuoi temi + css_and_html: + page_title: CSS e HTML + custom_css: + label: CSS personalizzato + text: > + + head: + label: Testata + text: > + + header: + label: Intestazione + text: > + + footer: + label: Piè di pagina + text: Questo verrà inserito prima di </body>. + sidebar: + label: Barra laterale + text: Questo verrà inserito nella barra laterale. + login: + page_title: Accedi + membership: + title: Adesione + label: Consenti nuove registrazioni + text: Disattiva per impedire a chiunque di creare un nuovo account. + email_registration: + title: Registrazione email + label: Consenti registrazione email + text: Disattiva per impedire a chiunque di creare un nuovo account tramite email. + allowed_email_domains: + title: Domini email consentiti + text: "Domini email con cui gli utenti devono registrare gli account. Un dominio per riga. Verrà ignorato quando vuoto.\n" + private: + title: Privato + label: Login obbligatorio + text: Solo gli utenti registrati possono accedere a questa community. + password_login: + title: Password di accesso + label: Consenti il login di email e password + text: "ATTENZIONE: Se disattivi, potresti non essere in grado di accedere se non hai precedentemente configurato un altro metodo di login." + installed_plugins: + title: Plugin installati + plugin_link: I plugin estendono ed espandono le funzionalità. È possibile trovare plugin nel repository <1>Plugin. + filter: + all: Tutto + active: Attivo + inactive: Inattivo + outdated: Obsoleto + plugins: + label: Plugin + text: Seleziona un plugin esistente. + name: Nome + version: Versione + status: Stato + action: Azione + deactivate: Disattivare + activate: Attivare + settings: Impostazioni + settings_users: + title: Utenti + avatar: + label: Avatar Predefinito + text: Per gli utenti senza un proprio avatar personalizzato. + gravatar_base_url: + label: Gravatar Base URL + text: "\nURL della base API del provider Gravatar. Ignorato quando vuoto." + profile_editable: + title: Profilo modificabile + allow_update_display_name: + label: Consenti di cambiare il nome utente + allow_update_username: + label: Consenti di modificare il proprio nome utente + allow_update_avatar: + label: Consenti agli utenti di modificare l'immagine del profilo + allow_update_bio: + label: Consenti agli utenti di modificare la sezione "su di me" + allow_update_website: + label: Consenti agli utenti di modificare il proprio sito web + allow_update_location: + label: Consenti agli utenti di modificare la propria posizione + privilege: + title: Privilegi + level: + label: Livello di reputazione richiesto + text: Scegli la reputazione richiesta per i privilegi + msg: + should_be_number: l'input dovrebbe essere un numero + number_larger_1: il numero dovrebbe essere uguale o superiore a 1 + badges: + action: Azione + active: Attivo + activate: Attivare + all: Tutto + awards: Ricompense + deactivate: Disattivare + filter: + placeholder: Filtra per nome, badge:id + group: Gruppo + inactive: Inattivo + name: Nome + show_logs: Visualizza i log + status: Stato + title: Badges + form: + optional: (opzionale) + empty: non può essere vuoto + invalid: non è valido + btn_submit: Salva + not_found_props: "Proprietà richiesta {{ key }} non trovata." + select: Seleziona + page_review: + review: Revisione + proposed: Proposto + question_edit: Edita domanda + answer_edit: Edita risposta + tag_edit: Edita tag + edit_summary: Edita il riepilogo + edit_question: Edita la domanda + edit_answer: Edita la risposta + edit_tag: Edita tag + empty: Nessuna attività di revisione rimasta. + approve_revision_tip: Approvi questa revisione? + approve_flag_tip: Approvi questo contrassegno? + approve_post_tip: Approvi questo post? + approve_user_tip: Approvate questo utente? + suggest_edits: Modifiche suggerite + flag_post: Post contrassegnato + flag_user: Utente contrassegno + queued_post: "Posta in coda\n" + queued_user: Utente in coda + filter_label: Digita + reputation: reputazione + flag_post_type: Post contrassegnato come {{ type }}. + flag_user_type: Utente contrassegnato come {{ type }}. + edit_post: Modifica il post + list_post: Lista il post + unlist_post: Rimuovi post + timeline: + undeleted: Non cancellato + deleted: eliminato + downvote: voto negativo + upvote: voto a favore + accept: accetta + cancelled: Cancellato + commented: commentato + rollback: ripristino + edited: modificato + answered: risposto + asked: chiesto + closed: chiuso + reopened: riaperto + created: creato + pin: Fissa in cima + unpin: Rimosso dalla cima + show: elencato + hide: non elencato + title: "Cronologia per" + tag_title: "Timeline per" + show_votes: "Mostra voti" + n_or_a: N/D + title_for_question: "Timeline per" + title_for_answer: "Timeline per rispondere a {{ title }} di {{ author }}" + title_for_tag: "Timeline per tag" + datetime: Data e ora + type: Tipo + by: Di + comment: Commento + no_data: "Non abbiamo trovato nulla" + users: + title: Utenti + users_with_the_most_reputation: Utenti con i punteggi di reputazione più alti questa settimana + users_with_the_most_vote: Utenti che hanno votato di più questa settimana + staffs: Lo staff della community + reputation: reputazione + votes: voti + prompt: + leave_page: Sei sicuro di voler lasciare questa pagina? + changes_not_save: Le modifiche potrebbero non essere salvate. + draft: + discard_confirm: Sei sicuro di voler eliminare la bozza? + messages: + post_deleted: Questo post è stato eliminato. + post_cancel_deleted: Questo post è stato ripristinato. + post_pin: Questo post è stato selezionato. + post_unpin: Questo post è stato sbloccato. + post_hide_list: Questo post è stato nascosto dall'elenco. + post_show_list: Questo post è stato mostrato in elenco. + post_reopen: Questo post è stato riaperto. + post_list: Questo post è stato inserito. + post_unlist: Questo post è stato rimosso. + post_pending: Il tuo post è in attesa di revisione. Sarà visibile dopo essere stato approvato. + post_closed: Questo post è stato chiuso. + answer_deleted: Questa risposta è stata eliminata. + answer_cancel_deleted: Questa risposta è stata ripristinata. + change_user_role: Il ruolo di questo utente è stato cambiato. + user_inactive: Questo utente è già inattivo. + user_normal: Questo utente è già normale. + user_suspended: Questo utente è stato sospeso. + user_deleted: Questo utente è stato eliminato. + badge_activated: Questo badge è stato attivato. + badge_inactivated: Questo badge è stato disattivato. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" diff --git a/i18n/ja_JP.yaml b/i18n/ja_JP.yaml new file mode 100644 index 000000000..eb8bf19f6 --- /dev/null +++ b/i18n/ja_JP.yaml @@ -0,0 +1,2342 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: 成功 + unknown: + other: 不明なエラー + request_format_error: + other: リクエスト形式が無効です。 + unauthorized_error: + other: 権限がありません。 + database_error: + other: データサーバーエラー + forbidden_error: + other: アクセス権限がありません。 + duplicate_request_error: + other: 重複しています + action: + report: + other: 通報 + edit: + other: 編集 + delete: + other: 削除 + close: + other: 解決済み + reopen: + other: 再オープン + forbidden_error: + other: アクセス権限がありません。 + pin: + other: ピン留めする + hide: + other: 限定公開にする + unpin: + other: ピン留め解除 + show: + other: 限定公開を解除する + invite_someone_to_answer: + other: 編集 + undelete: + other: 復元する + merge: + other: Merge + role: + name: + user: + other: ユーザー + admin: + other: 管理者 + moderator: + other: モデレーター + description: + user: + other: 一般的なアクセスしか持ちません。 + admin: + other: すべてにアクセスできる大いなる力を持っています。 + moderator: + other: 管理者以外のすべての投稿へのアクセス権を持っています。 + privilege: + level_1: + description: + other: レベル1 必要最低の評判レベルで利用可(クローズサイト・グループ・特定の人数下での利用) + level_2: + description: + other: レベル2 少しだけ評判レベルが必要(スタートアップコミュニティ・不特定多数の人数での利用) + level_3: + description: + other: レベル3 高い評判レベルが必要(成熟したコミュニティ) + level_custom: + description: + other: カスタムレベル + rank_question_add_label: + other: 質問する + rank_answer_add_label: + other: 回答を書く + rank_comment_add_label: + other: コメントを書く + rank_report_add_label: + other: 通報 + rank_comment_vote_up_label: + other: コメントを高評価 + rank_link_url_limit_label: + other: 一度に2つ以上のリンクを投稿する + rank_question_vote_up_label: + other: 質問を高評価 + rank_answer_vote_up_label: + other: 回答を高評価 + rank_question_vote_down_label: + other: 質問を低評価 + rank_answer_vote_down_label: + other: 回答を低評価 + rank_invite_someone_to_answer_label: + other: 誰かを回答に招待する + rank_tag_add_label: + other: 新しいタグを作成 + rank_tag_edit_label: + other: タグの説明を編集(レビューが必要) + rank_question_edit_label: + other: 他の質問を編集(レビューが必要) + rank_answer_edit_label: + other: 他の回答を編集(レビューが必要) + rank_question_edit_without_review_label: + other: レビューなしで他の質問を編集する + rank_answer_edit_without_review_label: + other: レビューなしで他の回答を編集する + rank_question_audit_label: + other: 質問の編集をレビュー + rank_answer_audit_label: + other: 回答の編集をレビュー + rank_tag_audit_label: + other: タグの編集をレビュー + rank_tag_edit_without_review_label: + other: レビューなしでタグの説明を編集 + rank_tag_synonym_label: + other: タグの同義語を管理する + email: + other: メールアドレス + e_mail: + other: メールアドレス + password: + other: パスワード + pass: + other: パスワード + old_pass: + other: Current password + original_text: + other: 投稿 + email_or_password_wrong_error: + other: メールアドレスとパスワードが一致しません。 + error: + common: + invalid_url: + other: 無効なURL + status_invalid: + other: 無効なステータス + password: + space_invalid: + other: パスワードにスペースを含めることはできません。 + admin: + cannot_update_their_password: + other: パスワードは変更できません。 + cannot_edit_their_profile: + other: プロフィールを変更できません。 + cannot_modify_self_status: + other: ステータスを変更できません。 + email_or_password_wrong: + other: メールアドレスとパスワードが一致しません。 + answer: + not_found: + other: 回答が見つかりません。 + cannot_deleted: + other: 削除する権限がありません。 + cannot_update: + other: 更新する権限がありません。 + question_closed_cannot_add: + other: 質問はクローズされて、追加できません。 + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: コメントを編集することはできません。 + not_found: + other: コメントが見つかりません。 + cannot_edit_after_deadline: + other: コメント時間が長すぎて変更できません。 + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: メールアドレスは既に存在しています。 + need_to_be_verified: + other: 電子メールを確認する必要があります。 + verify_url_expired: + other: メール認証済みURLの有効期限が切れています。メールを再送信してください。 + illegal_email_domain_error: + other: そのメールドメインからのメールは許可されていません。別のメールアドレスを使用してください。 + lang: + not_found: + other: 言語ファイルが見つかりません。 + object: + captcha_verification_failed: + other: Captchaが間違っています。 + disallow_follow: + other: フォローが許可されていません。 + disallow_vote: + other: 投票が許可されていません。 + disallow_vote_your_self: + other: 自分の投稿には投票できません。 + not_found: + other: オブジェクトが見つかりません。 + verification_failed: + other: 認証に失敗しました。 + email_or_password_incorrect: + other: メールアドレスとパスワードが一致しません。 + old_password_verification_failed: + other: 古いパスワードの確認に失敗しました。 + new_password_same_as_previous_setting: + other: 新しいパスワードは前のパスワードと同じです。 + already_deleted: + other: この投稿は削除されました。 + meta: + object_not_found: + other: メタオブジェクトが見つかりません + question: + already_deleted: + other: この投稿は削除されました。 + under_review: + other: あなたの投稿はレビュー待ちです。承認されると表示されます。 + not_found: + other: 質問が見つかりません。 + cannot_deleted: + other: 削除する権限がありません。 + cannot_close: + other: クローズする権限がありません。 + cannot_update: + other: 更新する権限がありません。 + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: 評判ランクが条件を満たしていません + vote_fail_to_meet_the_condition: + other: フィードバックをありがとうございます。投票には少なくとも {{.Rank}} の評判が必要です。 + no_enough_rank_to_operate: + other: 少なくとも {{.Rank}} の評判が必要です。 + report: + handle_failed: + other: レポートの処理に失敗しました。 + not_found: + other: レポートが見つかりません。 + tag: + already_exist: + other: タグは既に存在します。 + not_found: + other: タグが見つかりません。 + recommend_tag_not_found: + other: おすすめタグは存在しません。 + recommend_tag_enter: + other: 少なくとも 1 つの必須タグを入力してください。 + not_contain_synonym_tags: + other: 同義語のタグを含めないでください。 + cannot_update: + other: 更新する権限がありません。 + is_used_cannot_delete: + other: 使用中のタグは削除できません。 + cannot_set_synonym_as_itself: + other: 現在のタグの同義語をそのものとして設定することはできません。 + smtp: + config_from_name_cannot_be_email: + other: Fromの名前はメールアドレスにできません。 + theme: + not_found: + other: テーマが見つかりません。 + revision: + review_underway: + other: 現在編集できません。レビューキューにバージョンがあります。 + no_permission: + other: 編集する権限がありません。 + user: + external_login_missing_user_id: + other: サードパーティのプラットフォームは一意のユーザーIDを提供していないため、ログインできません。ウェブサイト管理者にお問い合わせください。 + external_login_unbinding_forbidden: + other: ログインを削除する前に、アカウントのログインパスワードを設定してください。 + email_or_password_wrong: + other: + other: メールアドレスとパスワードが一致しません。 + not_found: + other: ユーザーが見つかりません。 + suspended: + other: このユーザーは凍結されています + username_invalid: + other: 無効なユーザー名です! + username_duplicate: + other: ユーザー名は既に使用されています! + set_avatar: + other: アバターを設定できませんでした + cannot_update_your_role: + other: ロールを変更できません + not_allowed_registration: + other: 現在、このサイトは新規登録を受け付けておりません + not_allowed_login_via_password: + other: 現在、このサイトはパスワードでログインできません + access_denied: + other: アクセスが拒否されました + page_access_denied: + other: このページへのアクセス権がありません + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "一度に追加するユーザーの数は、1 -{{.MaxAmount}} の範囲にする必要があります。" + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: configの読み込みに失敗しました + database: + connection_failed: + other: データベースの接続が失敗しました + create_table_failed: + other: テーブルの作成に失敗しました + install: + create_config_failed: + other: config.yaml を作成できません。 + upload: + unsupported_file_format: + other: サポートされていないファイル形式です。 + site_info: + config_not_found: + other: configが見つかりません。 + badge: + object_not_found: + other: バッジオブジェクトが見つかりません + reason: + spam: + name: + other: スパム + desc: + other: この投稿は広告です。現在のトピックには有用ではありません。 + rude_or_abusive: + name: + other: 誹謗中傷 + desc: + other: "合理的な人は、このコンテンツを尊重する言説には不適切と判断するでしょう。" + a_duplicate: + name: + other: 重複 + desc: + other: この質問は以前に質問されており、すでに回答があります。 + placeholder: + other: 既存の質問リンクを入力してください + not_a_answer: + name: + other: 回答では無い + desc: + other: "これは答えとして投稿されましたが、質問に答えようとしません。 それはおそらく編集、コメント、別の質問、または完全に削除されるべきです。" + no_longer_needed: + name: + other: 必要では無い + desc: + other: このコメントは古く、この投稿とは関係がありません。 + something: + name: + other: その他 + desc: + other: 上記以外の理由でスタッフの注意が必要です。 + placeholder: + other: あなたが懸念していることを私たちに教えてください + community_specific: + name: + other: コミュニティ固有の理由です + desc: + other: この質問はコミュニティガイドラインを満たしていません + not_clarity: + name: + other: 詳細や明快さが必要です + desc: + other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。 + looks_ok: + name: + other: LGTM + desc: + other: この投稿はそのままで良く、改善する必要はありません! + needs_edit: + name: + other: 編集する必要があったため変更しました。 + desc: + other: 自分自身でこの投稿の問題を改善し修正します。 + needs_close: + name: + other: クローズする必要がある + desc: + other: クローズされた質問は回答できませんが、編集、投票、コメントはできます。 + needs_delete: + name: + other: 削除が必要です + desc: + other: この投稿は削除されました + question: + close: + duplicate: + name: + other: スパム + desc: + other: この質問は以前に質問されており、すでに回答があります。 + guideline: + name: + other: コミュニティ固有の理由です + desc: + other: この質問はコミュニティガイドラインを満たしていません + multiple: + name: + other: 詳細や明快さが必要です + desc: + other: この質問には現在複数の質問が含まれています。1つの問題にのみ焦点を当てる必要があります。 + other: + name: + other: その他 + desc: + other: 上記以外の理由でスタッフの注意が必要です。 + operation_type: + asked: + other: 質問済み + answered: + other: 回答済み + modified: + other: 修正済み + deleted_title: + other: 質問を削除 + questions_title: + other: 質問 + tag: + tags_title: + other: タグ + no_description: + other: タグには説明がありません。 + notification: + action: + update_question: + other: 質問を更新 + answer_the_question: + other: 回答済みの質問 + update_answer: + other: 回答を更新 + accept_answer: + other: 承認された回答 + comment_question: + other: コメントされた質問 + comment_answer: + other: コメントされた回答 + reply_to_you: + other: あなたへの返信 + mention_you: + other: メンションされました + your_question_is_closed: + other: あなたの質問はクローズされました + your_question_was_deleted: + other: あなたの質問は削除されました + your_answer_was_deleted: + other: あなたの質問は削除されました + your_comment_was_deleted: + other: あなたのコメントは削除されました + up_voted_question: + other: 質問を高評価 + down_voted_question: + other: 質問を低評価 + up_voted_answer: + other: 回答を高評価 + down_voted_answer: + other: 回答を低評価 + up_voted_comment: + other: コメントを高評価 + invited_you_to_answer: + other: あなたを回答に招待しました + earned_badge: + other: '"{{.BadgeName}}"バッジを獲得しました' + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] 新しいメールアドレスを確認してください" + body: + other: "{{.SiteName}}の新しいメールアドレスを確認しリンクをクリックしてください。
\n{{.ChangeEmailUrl}}

\n\n身に覚えがない場合はこのメールを無視してください。\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。" + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} があなたの質問に回答しました" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}}で確認

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} があなたを回答に招待しました" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
あなたなら答えを知っているかもしれません。

\n{{.SiteName}}で確認

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\n配信停止\n" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} があなたの投稿にコメントしました" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nで確認{{.SiteName}}

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n\n配信停止" + new_question: + title: + other: "[{{.SiteName}}] 新しい質問: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] パスワードリセット" + body: + other: "{{.SiteName}}であなたのパスワードをリセットしようとしました。

\n\nもしお心当たりがない場合は、このメールを無視してください。

\n\n新しいパスワードを設定するには、以下のリンクをクリックしてください。
\n{{.PassResetUrl}}\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

\n" + register: + title: + other: "[{{.SiteName}}] 新しいアカウントを確認" + body: + other: "{{.SiteName}}へようこそ!

\n\n以下のリンクをクリックして、新しいアカウントを確認・有効化してください。
\n{{.RegisterUrl}}

\n\n上記のリンクがクリックできない場合は、リンクをコピーしてブラウザのアドレスバーに貼り付けてみてください。\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

" + test: + title: + other: "[{{.SiteName}}] テストメール" + body: + other: "これはテストメールです。\n

\n\n--
\n注: これはシステムからの自動メールです。このメッセージに返信しないでください。

" + action_activity_type: + upvote: + other: 高評価 + upvoted: + other: 高評価しました + downvote: + other: 低評価 + downvoted: + other: 低評価しました + accept: + other: 承認 + accepted: + other: 承認済み + edit: + other: 編集 + review: + queued_post: + other: キューに入れられた投稿 + flagged_post: + other: 投稿を通報 + suggested_post_edit: + other: 提案された編集 + reaction: + tooltip: + other: "{{ .Names }} と {{ .Count }} もっと..." + badge: + default_badges: + autobiographer: + name: + other: 自伝作家 + desc: + other: プロファイル 情報を入力しました。 + certified: + name: + other: 認定済み + desc: + other: 新しいユーザーがチュートリアルを完了しました。 + editor: + name: + other: 編集者 + desc: + other: 最初の投稿の編集 + first_flag: + name: + other: 初めての報告 + desc: + other: 初めての報告 + first_upvote: + name: + other: はじめての高評価 + desc: + other: はじめて投稿に高評価した + first_link: + name: + other: はじめてのリンク + desc: + other: First added a link to another post. + first_reaction: + name: + other: 初めてのリアクション + desc: + other: はじめて投稿にリアクションした + first_share: + name: + other: はじめての共有 + desc: + other: はじめて投稿を共有した + scholar: + name: + other: 研究生 + desc: + other: 質問をして回答が承認された + commentator: + name: + other: コメントマン + desc: + other: 5つコメントをした + new_user_of_the_month: + name: + other: 今月の新しいユーザー + desc: + other: 最初の月に優れた貢献 + read_guidelines: + name: + other: ガイドラインを読んだ + desc: + other: '「コミュニティガイドライン」をご覧ください。' + reader: + name: + other: リーダー + desc: + other: 10以上の回答を持つトピックのすべての回答を読んだ + welcome: + name: + other: ようこそ! + desc: + other: 高評価をされた + nice_share: + name: + other: Nice Share + desc: + other: 25人の訪問者と投稿を共有した + good_share: + name: + other: Good Share + desc: + other: 300の訪問者と投稿を共有した + great_share: + name: + other: Great Share + desc: + other: 1000人の訪問者と投稿を共有した + out_of_love: + name: + other: お隣さん + desc: + other: 1日に50票いれた + higher_love: + name: + other: お友達 + desc: + other: 5日目に50回投票した + crazy_in_love: + name: + other: 崇拝 + desc: + other: 一日に50回投票を20回した + promoter: + name: + other: プロモーター + desc: + other: ユーザーを招待した + campaigner: + name: + other: キャンペーン + desc: + other: 3人のベーシックユーザーを招待しました。 + champion: + name: + other: チャンピオン + desc: + other: 5人のメンバーを招待しました。 + thank_you: + name: + other: Thank you + desc: + other: 投稿が20件!投票数が10件! + gives_back: + name: + other: 返品 + desc: + other: 投稿が100件 !?!? 投票数が100件 !?!? + empathetic: + name: + other: 共感性 + desc: + other: 500の投稿を投票し、1000の投票を与えた。 + enthusiast: + name: + other: 楽天家 + desc: + other: 10日間連続ログイン + aficionado: + name: + other: Aficionado + desc: + other: 100日連続ログイン!!! + devotee: + name: + other: 献身者 + desc: + other: 365日連続訪問!!!!!!!! + anniversary: + name: + other: 周年記念 + desc: + other: 年に一回は... + appreciated: + name: + other: ありがとう! + desc: + other: 20件の投稿に1件の投票を受け取った + respected: + name: + other: 尊敬される + desc: + other: 100件の投稿で2件の投票を受け取った + admired: + name: + other: 崇拝された + desc: + other: 300の投稿に5票を獲得した + solved: + name: + other: 解決 + desc: + other: 答えを受け入れられた + guidance_counsellor: + name: + other: アドバイザー + desc: + other: 10個の回答が承認された + know_it_all: + name: + other: 物知り博士 + desc: + other: 50個の回答が承認された + solution_institution: + name: + other: 解決機関 + desc: + other: 150個の回答が承認された + nice_answer: + name: + other: 素敵な回答 + desc: + other: 回答スコアは10以上!! + good_answer: + name: + other: 良い回答 + desc: + other: 回答スコアは25以上!?! + great_answer: + name: + other: 素晴らしい回答 + desc: + other: 回答スコアは50以上!!!!1 + nice_question: + name: + other: ナイスな質問 + desc: + other: 質問スコアは10以上!! + good_question: + name: + other: よい質問 + desc: + other: 質問スコアは25以上!?! + great_question: + name: + other: 素晴らしい質問 + desc: + other: 50人の閲覧者!! + popular_question: + name: + other: 人気のある質問 + desc: + other: 500人の閲覧者!!! + notable_question: + name: + other: 注目すべき質問 + desc: + other: 1,000人の閲覧者!!!! + famous_question: + name: + other: 偉大な質問 + desc: + other: 5,000人の閲覧者!!!!! + popular_link: + name: + other: 人気のリンク + desc: + other: 外部リンクを50回クリック + hot_link: + name: + other: 激アツリンク + desc: + other: 外部リンクを300回クリック + famous_link: + name: + other: 有名なリンク + desc: + other: 外部リンクを100回クリック + default_badge_groups: + getting_started: + name: + other: はじめに + community: + name: + other: コミュニティ + posting: + name: + other: 投稿中 +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 書式設定 + desc: \n + pagination: + prev: 前へ + next: 次へ + page_title: + question: 質問 + questions: 質問 + tag: タグ + tags: タグ + tag_wiki: タグ wiki + create_tag: タグを作成 + edit_tag: タグを編集 + ask_a_question: Create Question + edit_question: 質問を編集 + edit_answer: 回答を編集 + search: 検索 + posts_containing: 記事を含む投稿 + settings: 設定 + notifications: お知らせ + login: ログイン + sign_up: 新規登録 + account_recovery: アカウントの復旧 + account_activation: アカウント有効化 + confirm_email: メールアドレスを確認 + account_suspended: アカウントは凍結されています + admin: 管理者 + change_email: メールアドレスを変更 + install: 回答に応答する + upgrade: 回答を改善する + maintenance: ウェブサイトのメンテナンス + users: ユーザー + oauth_callback: 処理中 + http_404: HTTP エラー 404 + http_50X: HTTP エラー 500 + http_403: HTTP エラー 403 + logout: ログアウト + notifications: + title: 通知 + inbox: 受信トレイ + achievement: 実績 + new_alerts: 新しい通知 + all_read: すべて既読にする + show_more: もっと見る + someone: 誰か + inbox_type: + all: すべて + posts: 投稿 + invites: 招待 + votes: 投票 + answer: 回答 + question: 質問 + badge_award: バッジ + suspended: + title: あなたのアカウントは停止されています。 + until_time: "あなたのアカウントは {{ time }} まで停止されました。" + forever: このユーザーは永久に停止されました。 + end: コミュニティガイドラインを満たしていません。 + contact_us: お問い合わせ + editor: + blockquote: + text: 引用 + bold: + text: 強い + chart: + text: チャート + flow_chart: フローチャート + sequence_diagram: シーケンス図 + class_diagram: クラス図 + state_diagram: 状態図 + entity_relationship_diagram: ER図 + user_defined_diagram: ユーザー定義図 + gantt_chart: ガントチャート + pie_chart: 円グラフ + code: + text: コードサンプル + add_code: コードサンプルを追加 + form: + fields: + code: + label: コード + msg: + empty: Code を空にすることはできません。 + language: + label: 言語 + placeholder: 自動検出 + btn_cancel: キャンセル + btn_confirm: 追加 + formula: + text: 数式 + options: + inline: インライン数式 + block: ブロック数式 + heading: + text: 見出し + options: + h1: 見出し1 + h2: 見出し2 + h3: 見出し3 + h4: 見出し4 + h5: 見出し5 + h6: 見出し6 + help: + text: ヘルプ + hr: + text: 水平方向の罫線 + image: + text: 画像 + add_image: 画像を追加する + tab_image: 画像をアップロードする + form_image: + fields: + file: + label: 画像ファイル + btn: 画像を選択する + msg: + empty: ファイルは空にできません。 + only_image: 画像ファイルのみが許可されています。 + max_size: ファイルサイズは {{size}} MBを超えることはできません。 + desc: + label: 説明 + tab_url: 画像URL + form_url: + fields: + url: + label: 画像URL + msg: + empty: 画像のURLは空にできません。 + name: + label: 説明 + btn_cancel: キャンセル + btn_confirm: 追加 + uploading: アップロード中 + indent: + text: インデント + outdent: + text: アウトデント + italic: + text: 斜体 + link: + text: ハイパーリンク + add_link: ハイパーリンクを追加 + form: + fields: + url: + label: URL + msg: + empty: URLを入力してください。 + name: + label: 説明 + btn_cancel: キャンセル + btn_confirm: 追加 + ordered_list: + text: 順序付きリスト + unordered_list: + text: 箇条書きリスト + table: + text: ' テーブル' + heading: 見出し + cell: セル + file: + text: ファイルを添付 + not_supported: "そのファイルタイプをサポートしていません。 {{file_type}} でもう一度お試しください。" + max_size: "添付ファイルサイズは {{size}} MB を超えることはできません。" + close_modal: + title: この投稿を次のように閉じます... + btn_cancel: キャンセル + btn_submit: 送信 + remark: + empty: 入力してください。 + msg: + empty: 理由を選んでください。 + report_modal: + flag_title: この投稿を報告するフラグを立てています... + close_title: この投稿を次のように閉じます... + review_question_title: 質問の編集をレビュー + review_answer_title: 答えをレビューする + review_comment_title: レビューコメント + btn_cancel: キャンセル + btn_submit: 送信 + remark: + empty: 入力してください。 + msg: + empty: 理由を選んでください。 + not_a_url: URL形式が正しくありません。 + url_not_match: URL の原点が現在のウェブサイトと一致しません。 + tag_modal: + title: 新しいタグを作成 + form: + fields: + display_name: + label: 表示名 + msg: + empty: 表示名を入力してください。 + range: 表示名は最大 35 文字までです。 + slug_name: + label: URLスラッグ + desc: '文字セット「a-z」、「0-9」、「+ # -」を使用する必要があります。' + msg: + empty: URL スラッグを空にすることはできません。 + range: タイトルは最大35文字までです. + character: URL スラグに許可されていない文字セットが含まれています。 + desc: + label: 説明 + revision: + label: 修正 + edit_summary: + label: 概要を編集 + placeholder: >- + 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) + btn_cancel: キャンセル + btn_submit: 送信 + btn_post: 新しいタグを投稿 + tag_info: + created_at: 作成 + edited_at: 編集済 + history: 履歴 + synonyms: + title: 類義語 + text: 次のタグが再マップされます + empty: 同義語は見つかりません。 + btn_add: 同義語を追加 + btn_edit: 編集 + btn_save: 保存 + synonyms_text: 次のタグが再マップされます + delete: + title: このタグを削除 + tip_with_posts: >- +

同義語でタグを削除することはできません。

最初にこのタグから同義語を削除してください。

+ tip_with_synonyms: >- +

同義語でタグを削除することはできません。

最初にこのタグから同義語を削除してください。

+ tip: 本当に削除してもよろしいですか? + close: クローズ + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: タグを編集 + default_reason: タグを編集 + default_first_reason: タグを追加 + btn_save_edits: 編集を保存 + btn_cancel: キャンセル + dates: + long_date: MMM D + long_date_with_year: "YYYY年MM月D日" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: 今 + x_seconds_ago: "{{count}}秒前" + x_minutes_ago: "{{count}}分前" + x_hours_ago: "{{count}}時間前" + hour: 時 + day: 日 + hours: 時 + days: 日 + month: month + months: months + year: year + reaction: + heart: ハート + smile: 笑顔 + frown: 眉をひそめる + btn_label: リアクションの追加または削除 + undo_emoji: '{{ emoji }} のリアクションを元に戻す' + react_emoji: '{{ emoji }} に反応する' + unreact_emoji: '{{ emoji }} に反応しない' + comment: + btn_add_comment: コメントを追加 + reply_to: 返信: + btn_reply: 返信 + btn_edit: 編集 + btn_delete: 削除 + btn_flag: フラグ + btn_save_edits: 編集内容を保存 + btn_cancel: キャンセル + show_more: "{{count}} 件のその他のコメント" + tip_question: >- + コメントを使用して、より多くの情報を求めたり、改善を提案したりします。コメントで質問に答えることは避けてください。 + tip_answer: >- + コメントを使用して他のユーザーに返信したり、変更を通知します。新しい情報を追加する場合は、コメントの代わりに投稿を編集します。 + tip_vote: 投稿に役に立つものを追加します + edit_answer: + title: 回答を編集 + default_reason: 回答を編集 + default_first_reason: 回答を追加 + form: + fields: + revision: + label: 修正 + answer: + label: 回答 + feedback: + characters: コンテンツは6文字以上でなければなりません。 + edit_summary: + label: 概要を編集 + placeholder: >- + 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) + btn_save_edits: 編集を保存 + btn_cancel: キャンセル + tags: + title: タグ + sort_buttons: + popular: 人気 + name: 名前 + newest: 最新 + button_follow: フォロー + button_following: フォロー中 + tag_label: 質問 + search_placeholder: タグ名でフィルタ + no_desc: タグには説明がありません。 + more: もっと見る + wiki: Wiki + ask: + title: Create Question + edit_title: 質問を編集 + default_reason: 質問を編集 + default_first_reason: Create question + similar_questions: 類似の質問 + form: + fields: + revision: + label: 修正 + title: + label: タイトル + placeholder: What's your topic? Be specific. + msg: + empty: タイトルを空にすることはできません。 + range: タイトルは最大150文字までです + body: + label: 本文 + msg: + empty: 本文を空にすることはできません。 + tags: + label: タグ + msg: + empty: 少なくとも一つ以上のタグが必要です。 + answer: + label: 回答 + msg: + empty: 回答は空欄にできません + edit_summary: + label: 概要を編集 + placeholder: >- + 簡単にあなたの変更を説明します(修正スペル、固定文法、改善されたフォーマット) + btn_post_question: 質問を投稿する + btn_save_edits: 編集内容を保存 + answer_question: ご自身の質問に答えてください + post_question&answer: 質問と回答を投稿する + tag_selector: + add_btn: タグを追加 + create_btn: 新しタグを作成 + search_tag: タグを検索 + hint: "Describe what your content is about, at least one tag is required." + no_result: 一致するタグはありません + tag_required_text: 必須タグ (少なくとも 1 つ) + header: + nav: + question: 質問 + tag: タグ + user: ユーザー + badges: バッジ + profile: プロフィール + setting: 設定 + logout: ログアウト + admin: 管理者 + review: レビュー + bookmark: ブックマーク + moderation: モデレーション + search: + placeholder: 検索 + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: 変更 + loading: 読み込み中… + pic_auth_code: + title: CAPTCHA + placeholder: 上記のテキストを入力してください + msg: + empty: Code を空にすることはできません。 + inactive: + first: >- + {{mail}}にアクティベーションメールを送信しました。メールの指示に従ってアカウントをアクティベーションしてください。 + info: "届かない場合は、迷惑メールフォルダを確認してください。" + another: >- + {{mail}}に別のアクティベーションメールを送信しました。 到着には数分かかる場合があります。スパムフォルダを確認してください。 + btn_name: 認証メールを再送信 + change_btn_name: メールアドレスを変更 + msg: + empty: 空にすることはできません + resend_email: + url_label: 本当に認証メールを再送信してもよろしいですか? + url_text: ユーザーに上記のアクティベーションリンクを渡すこともできます。 + login: + login_to_continue: ログインして続行 + info_sign: アカウントをお持ちではありませんか?<1>サインアップ + info_login: すでにアカウントをお持ちですか?<1>ログイン + agreements: 登録することにより、<1>プライバシーポリシーおよび<3>サービス規約に同意するものとします。 + forgot_pass: パスワードをお忘れですか? + name: + label: 名前 + msg: + empty: 名前を空にすることはできません。 + range: 2~30文字の名前を設定してください。 + character: '文字セット "a-z", "A-Z", "0-9", " - " を使用する必要があります。' + email: + label: メールアドレス + msg: + empty: メールアドレスを入力してください。 + password: + label: パスワード + msg: + empty: パスワードを入力してください。 + different: 入力されたパスワードが一致しません + account_forgot: + page_title: パスワードを忘れた方はこちら + btn_name: 回復用のメールを送る + send_success: >- + アカウントが {{mail}}と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。 + email: + label: メールアドレス + msg: + empty: メールアドレスを入力してください。 + change_email: + btn_cancel: キャンセル + btn_update: メールアドレスを更新 + send_success: >- + アカウントが {{mail}}と一致する場合は、パスワードをすぐにリセットする方法に関するメールが送信されます。 + email: + label: 新しいメールアドレス + msg: + empty: メールアドレス欄を空白にしておくことはできません + oauth: + connect: '{{ auth_name }} と接続' + remove: '{{ auth_name }} を削除' + oauth_bind_email: + subtitle: アカウントに回復用のメールアドレスを追加します。 + btn_update: メールアドレスを更新 + email: + label: メールアドレス + msg: + empty: メールアドレスは空にできません。 + modal_title: このメールアドレスはすでに存在しています。 + modal_content: このメールアドレスは既に登録されています。既存のアカウントに接続してもよろしいですか? + modal_cancel: メールアドレスを変更する + modal_confirm: 既存のアカウントに接続 + password_reset: + page_title: パスワード再設定 + btn_name: パスワードをリセット + reset_success: >- + パスワードを変更しました。ログインページにリダイレクトされます。 + link_invalid: >- + 申し訳ありませんが、このパスワードリセットのリンクは無効になりました。パスワードが既にリセットされている可能性がありますか? + to_login: ログインページへ + password: + label: パスワード + msg: + empty: パスワードを入力してください。 + length: 長さは 8 から 32 の間である必要があります + different: 両側に入力されたパスワードが一致しません + password_confirm: + label: 新しいパスワードの確認 + settings: + page_title: 設定 + goto_modify: 変更を開く + nav: + profile: プロフィール + notification: お知らせ + account: アカウント + interface: 外観 + profile: + heading: プロフィール + btn_name: 保存 + display_name: + label: 表示名 + msg: 表示名は必須です。 + msg_range: Display name must be 2-30 characters in length. + username: + label: ユーザー名 + caption: ユーザーは "@username" としてあなたをメンションできます。 + msg: ユーザー名は空にできません。 + msg_range: Username must be 2-30 characters in length. + character: '文字セット "a-z", "0-9", " - . _" を使用してください。' + avatar: + label: プロフィール画像 + gravatar: Gravatar + gravatar_text: 画像を変更できます: + custom: カスタム + custom_text: 画像をアップロードできます。 + default: システム + msg: アバターをアップロードしてください + bio: + label: 自己紹介 + website: + label: ウェブサイト + placeholder: "http://example.com" + msg: ウェブサイトの形式が正しくありません + location: + label: ロケーション + placeholder: "都道府県,国" + notification: + heading: メール通知 + turn_on: ONにする + inbox: + label: 受信トレイの通知 + description: 質問、コメント、招待状などへの回答。 + all_new_question: + label: すべての新規質問 + description: すべての新しい質問の通知を受け取ります。週に最大50問まで。 + all_new_question_for_following_tags: + label: 以下のタグに対するすべての新しい質問 + description: タグをフォローするための新しい質問の通知を受け取る。 + account: + heading: アカウント + change_email_btn: メールアドレスを変更する + change_pass_btn: パスワードを変更する + change_email_info: >- + このアドレスにメールを送信しました。メールの指示に従って確認処理を行ってください。 + email: + label: メールアドレス + new_email: + label: 新しいメールアドレス + msg: 新しいメールアドレスは空にできません。 + pass: + label: 現在のパスワード + msg: パスワードは空白にできません + password_title: パスワード + current_pass: + label: 現在のパスワード + msg: + empty: 現在のパスワードが空欄です + length: 長さは 8 から 32 の間である必要があります. + different: パスワードが一致しません。 + new_pass: + label: 新しいパスワード + pass_confirm: + label: 新しいパスワードの確認 + interface: + heading: 外観 + lang: + label: インタフェース言語 + text: ユーザーインターフェイスの言語。ページを更新すると変更されます。 + my_logins: + title: ログイン情報 + label: これらのアカウントを使用してログインまたはこのサイトでサインアップします。 + modal_title: ログイン情報を削除 + modal_content: このログイン情報をアカウントから削除してもよろしいですか? + modal_confirm_btn: 削除 + remove_success: 削除に成功しました + toast: + update: 更新に成功しました + update_password: パスワードの変更に成功しました。 + flag_success: フラグを付けてくれてありがとう + forbidden_operate_self: 自分自身で操作することはできません + review: レビュー後にあなたのリビジョンが表示されます。 + sent_success: 正常に送信されました。 + related_question: + title: Related + answers: 回答 + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: 回答に招待する + add: ユーザーを追加 + search: ユーザーを検索 + question_detail: + action: 動作 + Asked: 質問済み + asked: 質問済み + update: 修正済み + edit: 編集済み + commented: コメントしました + Views: 閲覧回数 + Follow: フォロー + Following: フォロー中 + follow_tip: この質問をフォローして通知を受け取る + answered: 回答済み + closed_in: クローズまで + show_exist: 既存の質問を表示します + useful: 役に立った + question_useful: それは有用で明確です。 + question_un_useful: 不明確または有用ではない + question_bookmark: この質問をブックマークする + answer_useful: 役に立った + answer_un_useful: 役に立たない + answers: + title: 回答 + score: スコア + newest: 最新 + oldest: 古い順 + btn_accept: 承認 + btn_accepted: 承認済み + write_answer: + title: あなたの回答 + edit_answer: 既存の回答を編集する + btn_name: 回答を投稿する + add_another_answer: 別の回答を追加 + confirm_title: 回答を続ける + continue: 続行 + confirm_info: >- +

本当に別の答えを追加したいのですか?

代わりに、編集リンクを使って既存の答えを洗練させ、改善することができます。

+ empty: 回答は空欄にできません + characters: コンテンツは6文字以上でなければなりません。 + tips: + header_1: ご回答ありがとうございます。 + li1_1: " 必ず質問に答えてください。詳細を述べ、あなたの研究を共有してください。\n" + li1_2: 参考文献や個人的な経験による裏付けを取ること。. + header_2: しかし、 を避けてください... + li2_1: 助けを求める、説明を求める、または他の答えに応答する。 + reopen: + confirm_btn: 再オープン + title: この投稿を再度開く + content: 再オープンしてもよろしいですか? + list: + confirm_btn: 一覧 + title: この投稿の一覧 + content: 一覧表示してもよろしいですか? + unlist: + confirm_btn: 限定公開にする + title: この投稿を元に戻す + content: 本当に元に戻しますか? + pin: + title: この投稿をピン留めする + content: "グローバルに固定してもよろしいですか?\nこの投稿はすべての投稿リストの上部に表示されます。" + confirm_btn: ピン留めする + delete: + title: この投稿を削除 + question: >- +

承認された回答を削除することはお勧めしません。削除すると、今後の読者がこの知識を得られなくなってしまうからです。

承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか? + + answer_accepted: >- +

承認された回答を削除することはお勧めしません。削除すると、今後の読者がこの知識を得られなくなってしまうからです。

承認された回答を繰り返し削除すると、回答機能が制限され、アカウントがブロックされる場合があります。本当に削除しますか? + + other: 本当に削除してもよろしいですか? + tip_answer_deleted: この回答は削除されました + undelete_title: この投稿を元に戻す + undelete_desc: 本当に元に戻しますか? + btns: + confirm: 確認 + cancel: キャンセル + edit: 編集 + save: 保存 + delete: 削除 + undelete: 元に戻す + list: 限定公開を解除する + unlist: 限定公開にする + unlisted: 限定公開済み + login: ログイン + signup: 新規登録 + logout: ログアウト + verify: 認証 + create: Create + approve: 承認 + reject: 却下 + skip: スキップする + discard_draft: 下書きを破棄 + pinned: ピン留めしました + all: すべて + question: 質問 + answer: 回答 + comment: コメント + refresh: 更新 + resend: 再送 + deactivate: 無効化する + active: 有効 + suspend: 凍結 + unsuspend: 凍結解除 + close: クローズ + reopen: 再オープン + ok: OK + light: ライト + dark: ダーク + system_setting: システム設定 + default: 既定 + reset: リセット + tag: タグ + post_lowercase: 投稿 + filter: フィルター + ignore: 除外 + submit: 送信 + normal: 通常 + closed: クローズ済み + deleted: 削除済み + deleted_permanently: Deleted permanently + pending: 処理待ち + more: もっと見る + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: 検索結果 + keywords: キーワード + options: オプション + follow: フォロー + following: フォロー中 + counts: "結果:{{count}}" + counts_loading: "... Results" + more: もっと見る + sort_btns: + relevance: 関連性 + newest: 最新 + active: アクティブ + score: スコア + more: もっと見る + tips: + title: 詳細検索のヒント + tag: "<1>[tag] タグで検索" + user: "<1>ユーザー:ユーザー名作成者による検索" + answer: "<1>回答:0未回答の質問" + score: "<1>スコア:33以上のスコアを持つ投稿" + question: "<1>質問質問を検索" + is_answer: "<1>は答え答えを検索" + empty: 何も見つかりませんでした。
別のキーワードまたはそれ以下の特定のキーワードをお試しください。 + share: + name: シェア + copy: リンクをコピー + via: 投稿を共有... + copied: コピーしました + facebook: Facebookで共有 + twitter: Share to X + cannot_vote_for_self: 自分の投稿には投票できません。 + modal_confirm: + title: エラー... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: 新しいアカウントが確認されました。ホームページにリダイレクトされます。 + link: ホームページへ + oops: おっと! + invalid: 使用したリンクは機能しません。 + confirm_new_email: メールアドレスが更新されました。 + confirm_new_email_invalid: >- + 申し訳ありませんが、この確認リンクは無効です。メールアドレスが既に変更されている可能性があります。 + unsubscribe: + page_title: 購読解除 + success_title: 購読解除成功 + success_desc: 配信リストから削除され、その他のメールの送信が停止されました。 + link: 設定の変更 + question: + following_tags: フォロー中のタグ + edit: 編集 + save: 保存 + follow_tag_tip: タグに従って質問のリストをキュレートします。 + hot_questions: ホットな質問 + all_questions: すべての質問 + x_questions: "{{ count }} の質問" + x_answers: "{{ count }} の回答" + x_posts: "{{ count }} Posts" + questions: 質問 + answers: 回答 + newest: 最新 + active: 有効 + hot: 人気 + frequent: Frequent + recommend: おすすめ + score: スコア + unanswered: 未回答 + modified: 修正済み + answered: 回答済み + asked: 質問済み + closed: 解決済み + follow_a_tag: タグをフォロー + more: その他 + personal: + overview: 概要 + answers: 回答 + answer: 回答 + questions: 質問 + question: 質問 + bookmarks: ブックマーク + reputation: 評判 + comments: コメント + votes: 投票 + badges: バッジ + newest: 最新 + score: スコア + edit_profile: プロファイルを編集 + visited_x_days: "{{ count }}人の閲覧者" + viewed: 閲覧回数 + joined: 参加しました + comma: "," + last_login: 閲覧数 + about_me: 自己紹介 + about_me_empty: "// Hello, World !" + top_answers: よくある回答 + top_questions: よくある質問 + stats: 統計情報 + list_empty: 投稿が見つかりませんでした。
他のタブを選択しますか? + content_empty: 投稿が見つかりませんでした。 + accepted: 承認済み + answered: 回答済み + asked: 質問済み + downvoted: 低評価しました + mod_short: MOD + mod_long: モデレーター + x_reputation: 評価 + x_votes: 投票を受け取りました + x_answers: 回答 + x_questions: 質問 + recent_badges: 最近のバッジ + install: + title: Installation + next: 次へ + done: 完了 + config_yaml_error: config.yaml を作成できません。 + lang: + label: 言語を選択してください + db_type: + label: データベースエンジン + db_username: + label: ユーザー名 + placeholder: root + msg: ユーザー名は空にできません。 + db_password: + label: パスワード + placeholder: root + msg: パスワードを入力してください。 + db_host: + label: データベースのホスト。 + placeholder: "db:3306" + msg: データベースホストは空にできません。 + db_name: + label: データベース名 + placeholder: 回答 + msg: データベース名を空にすることはできません。 + db_file: + label: データベースファイル + placeholder: /data/answer.db + msg: データベースファイルは空にできません。 + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: config.yamlを作成 + label: config.yaml ファイルが作成されました。 + desc: >- + <1>/var/www/xxx/ディレクトリに<1>config.yamlファイルを手動で作成し、その中に次のテキストを貼り付けます。 + info: 完了したら、「次へ」ボタンをクリックします。 + site_information: サイト情報 + admin_account: 管理者アカウント + site_name: + label: サイト名: + msg: サイト名は空にできません. + msg_max_length: サイト名は最大30文字でなければなりません。 + site_url: + label: サイトURL + text: あなたのサイトのアドレス + msg: + empty: サイト URL は空にできません. + incorrect: サイトURLの形式が正しくありません。 + max_length: サイトのURLは最大512文字でなければなりません + contact_email: + label: 連絡先メール アドレス + text: このサイトを担当するキーコンタクトのメールアドレスです。 + msg: + empty: 連絡先メールアドレスを空にすることはできません。 + incorrect: 連絡先メールアドレスの形式が正しくありません。 + login_required: + label: 非公開 + switch: ログインが必要です + text: ログインしているユーザーのみがこのコミュニティにアクセスできます。 + admin_name: + label: 名前 + msg: 名前を空にすることはできません。 + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: パスワード + text: >- + ログインするにはこのパスワードが必要です。安全な場所に保存してください。 + msg: パスワードは空白にできません + msg_min_length: パスワードは8文字以上でなければなりません。 + msg_max_length: パスワードは最大 32 文字でなければなりません。 + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: メールアドレス + text: ログインするにはこのメールアドレスが必要です。 + msg: + empty: メールアドレスは空にできません。 + incorrect: メールアドレスの形式が正しくありません. + ready_title: サイトの準備ができました + ready_desc: >- + もっと設定を変更したいと思ったことがある場合は、<1>管理者セクションをご覧ください。サイトメニューで見つけてください。 + good_luck: "楽しんで、幸運を!" + warn_title: 警告 + warn_desc: >- + ファイル<1>config.yamlは既に存在します。このファイルのいずれかの設定アイテムをリセットする必要がある場合は、最初に削除してください。 + install_now: <1>今すぐインストールを試してみてください。 + installed: 既にインストール済みです + installed_desc: >- + 既にインストールされているようです。再インストールするには、最初に古いデータベーステーブルをクリアしてください。 + db_failed: データベースの接続が失敗しました + db_failed_desc: >- + これは、<1>設定内のデータベース情報を意味します。 amlファイルが正しくないか、データベースサーバーとの連絡先が確立できませんでした。ホストのデータベースサーバーがダウンしている可能性があります。 + counts: + views: ビュー + votes: 投票数 + answers: 回答 + accepted: 承認済み + page_error: + http_error: HTTP Error {{ code }} + desc_403: このページにアクセスする権限がありません。 + desc_404: 残念ながら、このページは存在しません。 + desc_50X: サーバーにエラーが発生し、リクエストを完了できませんでした。 + back_home: ホームページに戻ります + page_maintenance: + desc: "メンテナンス中です。まもなく戻ります。" + nav_menus: + dashboard: ダッシュボード + contents: コンテンツ + questions: 質問 + answers: 回答 + users: ユーザー + badges: バッジ + flags: フラグ + settings: 設定 + general: 一般 + interface: 外観 + smtp: SMTP + branding: ブランディング + legal: 法的事項 + write: 書き + tos: 利用規約 + privacy: プライバシー + seo: SEO + customize: カスタマイズ + themes: テーマ + login: ログイン + privileges: 特典 + plugins: プラグイン + installed_plugins: 使用中のプラグイン + apperance: Appearance + website_welcome: '{{site_name}} へようこそ' + user_center: + login: ログイン + qrcode_login_tip: QRコードをスキャンしてログインするには {{ agentName }} を使用してください。 + login_failed_email_tip: ログインに失敗しました。もう一度やり直す前に、このアプリがあなたのメール情報にアクセスすることを許可してください。 + badges: + modal: + title: お疲れ様でした! + content: 新しいバッジを獲得しました。 + close: クローズ + confirm: バッジを表示 + title: バッジ + awarded: 受賞済み + earned_×: 獲得×{{ number }} + ×_awarded: "{{ number }} 受賞" + can_earn_multiple: これを複数回獲得できます。 + earned: 獲得済み + admin: + admin_header: + title: 管理者 + dashboard: + title: ダッシュボード + welcome: Adminへようこそ! + site_statistics: サイト統計 + questions: "質問:" + resolved: "解決済み:" + unanswered: "未回答:" + answers: "回答:" + comments: "評論:" + votes: "投票:" + users: "ユーザー数:" + flags: "フラグ:" + reviews: "レビュー:" + site_health: サイトの状態 + version: "バージョン:" + https: "HTTPS:" + upload_folder: "フォルダを上げる" + run_mode: "実行中モード:" + private: 非公開 + public: 公開 + smtp: "SMTP:" + timezone: "Timezone:" + system_info: システム情報 + go_version: "バージョン:" + database: "データベース:" + database_size: "データベースのサイズ:" + storage_used: "使用されているストレージ" + uptime: "稼働時間:" + links: リンク + plugins: プラグイン + github: GitHub + blog: ブログ + contact: 連絡先 + forum: Forum + documents: ドキュメント + feedback: フィードバック + support: サポート + review: レビュー + config: 設定 + update_to: 更新日時 + latest: 最新 + check_failed: チェックに失敗しました + "yes": "はい" + "no": "いいえ" + not_allowed: 許可されていません + allowed: 許可 + enabled: 有効 + disabled: 無効 + writable: 書き込み可 + not_writable: 書き込み不可 + flags: + title: フラグ + pending: 処理待ち + completed: 完了済 + flagged: フラグ付き + flagged_type: フラグを立てた {{ type }} + created: 作成 + action: 動作 + review: レビュー + user_role_modal: + title: ユーザーロールを変更... + btn_cancel: キャンセル + btn_submit: 送信 + new_password_modal: + title: 新しいパスワードを設定 + form: + fields: + password: + label: パスワード + text: ユーザーはログアウトされ、再度ログインする必要があります。 + msg: パスワードの長さは 8 ~ 32 文字である必要があります。 + btn_cancel: キャンセル + btn_submit: 送信 + edit_profile_modal: + title: プロファイルを編集 + form: + fields: + display_name: + label: 表示名 + msg_range: Display name must be 2-30 characters in length. + username: + label: ユーザー名 + msg_range: Username must be 2-30 characters in length. + email: + label: メールアドレス + msg_invalid: 無効なメールアドレス + edit_success: 更新が成功しました + btn_cancel: キャンセル + btn_submit: 送信 + user_modal: + title: 新しいユーザーを追加 + form: + fields: + users: + label: ユーザーを一括追加 + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: '「名前、メールアドレス、パスワード」をカンマで区切ってください。' + msg: "ユーザーのメールアドレスを1行に1つ入力してください。" + display_name: + label: 表示名 + msg: 表示名の長さは 2 ~ 30 文字にする必要があります。 + email: + label: メールアドレス + msg: メールアドレスが無効です。 + password: + label: パスワード + msg: パスワードの長さは 8 ~ 32 文字である必要があります。 + btn_cancel: キャンセル + btn_submit: 送信 + users: + title: ユーザー + name: 名前 + email: メールアドレス + reputation: 評価 + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: ステータス + role: ロール + action: 操作 + change: 変更 + all: すべて + staff: スタッフ + more: もっと見る + inactive: 非アクティブ + suspended: 凍結済み + deleted: 削除済み + normal: 通常 + Moderator: モデレーター + Admin: 管理者 + User: ユーザー + filter: + placeholder: "ユーザー名でフィルタ" + set_new_password: 新しいパスワードを設定します。 + edit_profile: プロファイルを編集 + change_status: ステータスを変更 + change_role: ロールを変更 + show_logs: ログを表示 + add_user: ユーザを追加 + deactivate_user: + title: ユーザーを非アクティブにする + content: アクティブでないユーザーはメールアドレスを再認証する必要があります。 + delete_user: + title: このユーザの削除 + content: このユーザーを削除してもよろしいですか?これは永久的です! + remove: このコンテンツを削除 + label: すべての質問、回答、コメントなどを削除 + text: ユーザーのアカウントのみ削除したい場合は、これを確認しないでください。 + suspend_user: + title: ユーザーをサスペンドにする + content: 一時停止中のユーザーはログインできません。 + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: 質問 + unlisted: 限定公開済み + post: 投稿 + votes: 投票 + answers: 回答 + created: 作成 + status: ステータス + action: 動作 + change: 変更 + pending: 処理待ち + filter: + placeholder: "タイトル、質問:id でフィルター" + answers: + page_title: 回答 + post: 投稿 + votes: 投票 + created: 作成 + status: ステータス + action: 操作 + change: 変更 + filter: + placeholder: "タイトル、質問:id でフィルター" + general: + page_title: 一般 + name: + label: サイト名 + msg: サイト名は空にできません. + text: "タイトルタグで使用されるこのサイトの名前。" + site_url: + label: サイトURL + msg: サイト URL は空にできません. + validate: 正しいURLを入力してください。 + text: あなたのサイトのアドレス + short_desc: + label: 短いサイトの説明 + msg: 短いサイト説明は空にできません. + text: "ホームページのタイトルタグで使用されている簡単な説明。" + desc: + label: サイトの説明 + msg: サイト説明を空にすることはできません。 + text: "メタ説明タグで使用されるように、このサイトを1つの文で説明します。" + contact_email: + label: 連絡先メール アドレス + msg: 連絡先メールアドレスを空にすることはできません。 + validate: 連絡先のメールアドレスが無効です。 + text: このサイトを担当するキーコンタクトのメールアドレスです。 + check_update: + label: ソフトウェアアップデート + text: 自動的に更新を確認 + interface: + page_title: 外観 + language: + label: インタフェース言語 + msg: インターフェース言語は空にできません。 + text: ユーザーインターフェイスの言語。ページを更新すると変更されます。 + time_zone: + label: タイムゾーン + msg: タイムゾーンを空にすることはできません。 + text: あなたのタイムゾーンを選択してください。 + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: 差出人 + msg: 差出人メールアドレスは空にできません。 + text: 送信元のメールアドレス + from_name: + label: 差出人名 + msg: 差出人名は空にできません + text: メールの送信元の名前 + smtp_host: + label: SMTP ホスト + msg: SMTPホストは空にできません。 + text: メールサーバー + encryption: + label: 暗号化 + msg: 暗号化は空にできません。 + text: ほとんどのサーバではSSLが推奨されます。 + ssl: SSL + tls: TLS + none: なし + smtp_port: + label: SMTPポート + msg: SMTPポートは1〜65535でなければなりません。 + text: メールサーバーへのポート番号 + smtp_username: + label: SMTPユーザ名 + msg: SMTP ユーザー名を空にすることはできません。 + smtp_password: + label: SMTPパスワード + msg: SMTP パスワードを入力してください。 + test_email_recipient: + label: テストメールの受信者 + text: テスト送信を受信するメールアドレスを入力してください。 + msg: テストメールの受信者が無効です + smtp_authentication: + label: 認証を有効にする + title: SMTP認証 + msg: SMTP認証は空にできません。 + "yes": "はい" + "no": "いいえ" + branding: + page_title: ブランディング + logo: + label: ロゴ + msg: ロゴは空にできません。 + text: あなたのサイトの左上にあるロゴ画像。 高さが56、アスペクト比が3:1を超える広い矩形画像を使用します。 空白の場合、サイトタイトルテキストが表示されます。 + mobile_logo: + label: モバイルロゴ + text: サイトのモバイル版で使用されるロゴです。高さが56の横長の長方形の画像を使用してください。空白のままにすると、"ロゴ"設定の画像が使用されます。 + square_icon: + label: アイコン画像 + msg: アイコン画像は空にできません。 + text: メタデータアイコンのベースとして使用される画像。理想的には512x512より大きくなければなりません。 + favicon: + label: Favicon + text: あなたのサイトのファビコン。CDN上で正しく動作するにはpngである必要があります。 32x32にリサイズされます。空白の場合は、"正方形のアイコン"が使用されます。 + legal: + page_title: 法的情報 + terms_of_service: + label: 利用規約 + text: "ここで利用規約のサービスコンテンツを追加できます。すでに他の場所でホストされているドキュメントがある場合は、こちらにフルURLを入力してください。" + privacy_policy: + label: プライバシーポリシー + text: "ここにプライバシーポリシーの内容を追加できます。すでに他の場所でホストされているドキュメントを持っている場合は、こちらにフルURLを入力してください。" + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: 編集 + restrict_answer: + title: 回答を書く + label: 各ユーザーは同じ質問に対して1つの回答しか書けません + text: "ユーザが同じ質問に複数の回答を書き込めるようにするにはオフにします。これにより回答がフォーカスされていない可能性があります。" + recommend_tags: + label: おすすめタグ + text: "デフォルトでドロップダウンリストに推奨タグが表示されます。" + msg: + contain_reserved: "推奨されるタグは予約済みタグを含めることはできません" + required_tag: + title: 必須タグを設定 + label: 必須タグに「推奨タグ」を設定 + text: "新しい質問には少なくとも1つの推奨タグが必要です。" + reserved_tags: + label: 予約済みタグ + text: "予約済みのタグはモデレータのみ使用できます。" + image_size: + label: 画像ファイルの最大サイズ(MB) + text: "画像ファイルの最大アップロードサイズ。" + attachment_size: + label: 添付ファイルの最大サイズ (MB) + text: "添付ファイルの最大アップロードサイズ。" + image_megapixels: + label: 画像の最大解像度 + text: "画像ファイルに許可する最大メガピクセル数。" + image_extensions: + label: 認可された画像ファイルの拡張子 + text: "イメージ表示に許可されるファイル拡張子のリスト(コンマで区切り)" + attachment_extensions: + label: 認可された添付ファイルの拡張子 + text: "アップロードを許可するファイル拡張子のリスト(カンマ区切り)\n警告: アップロードを許可するとセキュリティ上の問題が発生する可能性があります。" + seo: + page_title: SEO + permalink: + label: 固定リンク + text: カスタム URL 構造は、ユーザビリティとリンクの前方互換性を向上させることができます。 + robots: + label: robots.txt + text: これにより、関連するサイト設定が永久に上書きされます。 + themes: + page_title: テーマ + themes: + label: テーマ + text: 既存のテーマを選択してください + color_scheme: + label: 配色 + navbar_style: + label: Navbar background style + primary_color: + label: メインカラー + text: テーマで使用される色を変更する + css_and_html: + page_title: CSS と HTML + custom_css: + label: カスタム CSS + text: > + + head: + label: ヘッド + text: > + + header: + label: ヘッダー + text: > + + footer: + label: フッター + text: これは </body> の前に挿入されます。 + sidebar: + label: サイドバー + text: サイドバーに挿入されます。 + login: + page_title: ログイン + membership: + title: メンバー + label: 新しい登録を許可する + text: 誰もが新しいアカウントを作成できないようにするには、オフにしてください。 + email_registration: + title: メールアドレスの登録 + label: メールアドレスの登録を許可 + text: オフにすると、メールで新しいアカウントを作成できなくなります。 + allowed_email_domains: + title: 許可されたメールドメイン + text: ユーザーがアカウントを登録する必要があるメールドメインです。1行に1つのドメインがあります。空の場合は無視されます。 + private: + title: 非公開 + label: ログインが必要です + text: ログインしているユーザーのみがこのコミュニティにアクセスできます。 + password_login: + title: パスワードログイン + label: メールアドレスとパスワードのログインを許可する + text: "警告: オフにすると、他のログイン方法を設定していない場合はログインできない可能性があります。" + installed_plugins: + title: インストール済みプラグイン + plugin_link: プラグインは機能を拡張します。<1>プラグインリポジトリにプラグインがあります。 + filter: + all: すべて + active: アクティブ + inactive: 非アクティブ + outdated: 期限切れ + plugins: + label: プラグイン + text: 既存のプラグインを選択します + name: 名前 + version: バージョン + status: ステータス + action: 操作 + deactivate: 非アクティブ化 + activate: アクティベート + settings: 設定 + settings_users: + title: ユーザー + avatar: + label: デフォルトのアバター + text: 自分のカスタムアバターのないユーザー向け。 + gravatar_base_url: + label: Gravatar Base URL + text: GravatarプロバイダーのAPIベースのURL。空の場合は無視されます。 + profile_editable: + title: プロフィール編集可能 + allow_update_display_name: + label: ユーザーが表示名を変更できるようにする + allow_update_username: + label: ユーザー名の変更を許可する + allow_update_avatar: + label: ユーザーのプロフィール画像の変更を許可する + allow_update_bio: + label: ユーザーが自分についての変更を許可する + allow_update_website: + label: ユーザーのウェブサイトの変更を許可する + allow_update_location: + label: ユーザーの位置情報の変更を許可する + privilege: + title: 特権 + level: + label: 評判の必要レベル + text: 特権に必要な評判を選択します + msg: + should_be_number: 入力は数値でなければなりません + number_larger_1: 数値は 1 以上でなければなりません + badges: + action: 操作 + active: アクティブ + activate: アクティベート + all: すべて + awards: 賞 + deactivate: 非アクティブ化 + filter: + placeholder: 名前、バッジ:id でフィルター + group: グループ + inactive: 非アクティブ + name: 名前 + show_logs: ログを表示 + status: ステータス + title: バッジ + form: + optional: (任意) + empty: 空にすることはできません + invalid: 無効です + btn_submit: 保存 + not_found_props: "必須プロパティ {{ key }} が見つかりません。" + select: 選択 + page_review: + review: レビュー + proposed: 提案された + question_edit: 質問の編集 + answer_edit: 回答の編集 + tag_edit: タグの編集 + edit_summary: 概要を編集 + edit_question: 質問を編集 + edit_answer: 回答を編集 + edit_tag: タグを編集 + empty: レビュータスクは残っていません。 + approve_revision_tip: この修正を承認しますか? + approve_flag_tip: このフラグを承認しますか? + approve_post_tip: この投稿を承認しますか? + approve_user_tip: このユーザーを承認しますか? + suggest_edits: 提案された編集 + flag_post: 報告された投稿 + flag_user: 報告されたユーザー + queued_post: キューに入れられた投稿 + queued_user: キューに入れられたユーザー + filter_label: タイプ + reputation: 評価 + flag_post_type: この投稿は {{ type }} として報告されました + flag_user_type: このユーザーは {{ type }} として報告されました + edit_post: 投稿を編集 + list_post: 投稿一覧 + unlist_post: 限定公開投稿 + timeline: + undeleted: 復元する + deleted: 削除済み + downvote: 低評価 + upvote: 高評価 + accept: 承認 + cancelled: キャンセル済み + commented: コメントしました + rollback: rollback + edited: 編集済み + answered: 回答済み + asked: 質問済み + closed: クローズ済み + reopened: 再オープン + created: 作成済み + pin: ピン留め済 + unpin: ピン留め解除 + show: 限定公開解除済み + hide: 限定公開済み + title: "履歴:" + tag_title: "タイムライン:" + show_votes: "投票を表示" + n_or_a: N/A + title_for_question: "タイムライン:" + title_for_answer: "{{ title }} の {{ author }} 回答のタイムライン" + title_for_tag: "タグのタイムライン:" + datetime: 日付時刻 + type: タイプ + by: By + comment: コメント + no_data: "何も見つけられませんでした" + users: + title: ユーザー + users_with_the_most_reputation: 今週最も高い評価スコアを持つユーザ + users_with_the_most_vote: 今週一番多く投票したユーザー + staffs: コミュニティのスタッフ + reputation: 評価 + votes: 投票 + prompt: + leave_page: このページから移動してもよろしいですか? + changes_not_save: 変更が保存されない可能性があります + draft: + discard_confirm: 下書きを破棄してもよろしいですか? + messages: + post_deleted: この投稿は削除されました。 + post_cancel_deleted: この投稿の削除が取り消されました。 + post_pin: この投稿はピン留めされています。 + post_unpin: この投稿のピン留めが解除されました。 + post_hide_list: この投稿は一覧から非表示になっています。 + post_show_list: この投稿は一覧に表示されています。 + post_reopen: この投稿は再オープンされました。 + post_list: この投稿は一覧に表示されています。 + post_unlist: この投稿は一覧に登録されていません。 + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: この投稿はクローズされました。 + answer_deleted: この回答は削除されました。 + answer_cancel_deleted: この回答の削除が取り消されました。 + change_user_role: このユーザーのロールが変更されました。 + user_inactive: このユーザーは既に無効です。 + user_normal: このユーザーは既に有効です。 + user_suspended: このユーザーは凍結されています。 + user_deleted: このユーザーは削除されました。 + badge_activated: このバッジは有効化されました。 + badge_inactivated: このバッジは無効化されています。 + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/ko_KR.yaml b/i18n/ko_KR.yaml new file mode 100644 index 000000000..97c254172 --- /dev/null +++ b/i18n/ko_KR.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: 성공. + unknown: + other: 알 수 없는 오류. + request_format_error: + other: 요청 형식이 잘못되었습니다. + unauthorized_error: + other: 권한이 없습니다. + database_error: + other: 데이터 서버 오류. + forbidden_error: + other: 접근 금지. + duplicate_request_error: + other: 중복된 제출입니다. + action: + report: + other: 신고하기 + edit: + other: 편집 + delete: + other: 삭제 + close: + other: 닫기 + reopen: + other: 다시 열기 + forbidden_error: + other: 접근 금지. + pin: + other: 고정 + hide: + other: 리스트 제거 + unpin: + other: 고정 해제 + show: + other: 리스트 + invite_someone_to_answer: + other: 편집 + undelete: + other: 삭제 취소 + merge: + other: 병합 + role: + name: + user: + other: 유저 + admin: + other: 관리자 + moderator: + other: 중재자 + description: + user: + other: 특별한 접근 권한이 없는 기본 사용자입니다. + admin: + other: 사이트에 대한 모든 권한을 가집니다. + moderator: + other: 관리자 설정을 제외한 모든 게시물에 접근할 수 있습니다. + privilege: + level_1: + description: + other: 레벨 1 (비공개 팀, 그룹에 적은 평판 필요) + level_2: + description: + other: 레벨 2 (스타트업 커뮤니티에 낮은 평판 필요) + level_3: + description: + other: 레벨 3 (성숙한 커뮤니티에 높은 평판 필요) + level_custom: + description: + other: 커스텀 레벨 + rank_question_add_label: + other: 질문하기 + rank_answer_add_label: + other: 답변하기 + rank_comment_add_label: + other: 댓글 작성하기 + rank_report_add_label: + other: 신고하기 + rank_comment_vote_up_label: + other: 댓글 추천 + rank_link_url_limit_label: + other: 한번에 2개 이상의 링크를 게시하세요 + rank_question_vote_up_label: + other: 질문 추천 + rank_answer_vote_up_label: + other: 답변 추천 + rank_question_vote_down_label: + other: 질문 비추천 + rank_answer_vote_down_label: + other: 답변 비추천 + rank_invite_someone_to_answer_label: + other: 답변할 사람 초대 + rank_tag_add_label: + other: 새 태그 만들기 + rank_tag_edit_label: + other: 태그 설명 편집 (검토 필요) + rank_question_edit_label: + other: 다른 사람의 질문 편집 (검토 필요) + rank_answer_edit_label: + other: 다른 사람의 답변 편집 (검토 필요) + rank_question_edit_without_review_label: + other: 검토 없이 다른 사람의 질문 편집 + rank_answer_edit_without_review_label: + other: 검토 없이 다른 사람의 답변 편집 + rank_question_audit_label: + other: 질문 편집 검토하기 + rank_answer_audit_label: + other: 답변 편집 검토하기 + rank_tag_audit_label: + other: 태그 편집 검토하기 + rank_tag_edit_without_review_label: + other: 검토 없이 태그 설명 편집 + rank_tag_synonym_label: + other: 태그 동의어 관리 + email: + other: 이메일 + e_mail: + other: 이메일 + password: + other: 비밀번호 + pass: + other: 비밀번호 + old_pass: + other: 현재 비밀번호 + original_text: + other: 이 게시물 + email_or_password_wrong_error: + other: 이메일과 비밀번호가 일치하지 않습니다. + error: + common: + invalid_url: + other: 잘못된 URL 입니다. + status_invalid: + other: 유효하지 않은 상태. + password: + space_invalid: + other: 암호에는 공백을 포함할 수 없습니다. + admin: + cannot_update_their_password: + other: 암호를 변경할 수 없습니다. + cannot_edit_their_profile: + other: 수정할 수 없습니다. + cannot_modify_self_status: + other: 상태를 변경할 수 없습니다. + email_or_password_wrong: + other: 이메일과 비밀번호가 일치하지 않습니다. + answer: + not_found: + other: 답변을 찾을 수 없습니다. + cannot_deleted: + other: 삭제 권한이 없습니다. + cannot_update: + other: 편집 권한이 없습니다. + question_closed_cannot_add: + other: 질문이 닫혔으며, 추가할 수 없습니다. + content_cannot_empty: + other: 답변 내용은 비워둘 수 없습니다. + comment: + edit_without_permission: + other: 편집이 가능하지 않은 댓글입니다. + not_found: + other: 댓글을 찾을 수 없습니다. + cannot_edit_after_deadline: + other: 수정 허용 시간을 초과했습니다. + content_cannot_empty: + other: 댓글 내용은 비워둘 수 없습니다. + email: + duplicate: + other: 이미 존재하는 이메일입니다. + need_to_be_verified: + other: 이메일을 확인해주세요. + verify_url_expired: + other: URL이 만료되었습니다. 이메일을 다시 보내주세요. + illegal_email_domain_error: + other: 해당 도메인을 사용할 수 없습니다. 다른 전자 메일을 사용하십시오. + lang: + not_found: + other: 언어 파일을 찾을 수 없습니다. + object: + captcha_verification_failed: + other: 잘못된 Captcha입니다. + disallow_follow: + other: 팔로우할 수 없습니다. + disallow_vote: + other: 추천할 수 없습니다. + disallow_vote_your_self: + other: 자신의 게시물에는 추천할 수 없습니다. + not_found: + other: 오브젝트를 찾을 수 없습니다. + verification_failed: + other: 확인 실패. + email_or_password_incorrect: + other: 이메일과 비밀번호가 일치하지 않습니다. + old_password_verification_failed: + other: 이전 암호 확인에 실패했습니다 + new_password_same_as_previous_setting: + other: 새 비밀번호는 이전 비밀번호와 다르게 입력해주세요. + already_deleted: + other: 이 게시물은 삭제되었습니다. + meta: + object_not_found: + other: 메타 객체를 찾을 수 없습니다 + question: + already_deleted: + other: 삭제된 게시물입니다. + under_review: + other: 검토를 기다리고 있습니다. 승인된 후에 볼 수 있습니다. + not_found: + other: 질문을 찾을 수 없습니다. + cannot_deleted: + other: 삭제 권한이 없습니다. + cannot_close: + other: 닫을 권한이 없습니다. + cannot_update: + other: 업데이트 권한이 없습니다. + content_cannot_empty: + other: 내용은 비워둘 수 없습니다. + rank: + fail_to_meet_the_condition: + other: 등급이 조건을 충족하지 못합니다. + vote_fail_to_meet_the_condition: + other: 피드백 감사합니다. 투표를 하려면 최소 {{.Rank}} 평판이 필요합니다. + no_enough_rank_to_operate: + other: 이 작업을 하려면 최소 {{.Rank}} 평판이 필요합니다. + report: + handle_failed: + other: 신고 처리에 실패했습니다. + not_found: + other: 신고를 찾을 수 없습니다. + tag: + already_exist: + other: 이미 존재하는 태그입니다. + not_found: + other: 태그를 찾을 수 없습니다. + recommend_tag_not_found: + other: 추천 태그가 존재하지 않습니다. + recommend_tag_enter: + other: 하나 이상의 태그를 입력해주세요. + not_contain_synonym_tags: + other: 동일한 태그를 포함하지 않아야 합니다. + cannot_update: + other: 편집 권한이 없습니다. + is_used_cannot_delete: + other: 사용 중인 태그는 삭제할 수 없습니다. + cannot_set_synonym_as_itself: + other: 현재 태그의 동의어로 자기 자신을 설정할 수 없습니다. + smtp: + config_from_name_cannot_be_email: + other: 발신자 이름은 이메일 주소가 될 수 없습니다. + theme: + not_found: + other: 테마를 찾을 수 없습니다. + revision: + review_underway: + other: 검토 대기열에 있기 때문에 현재 편집할 수 없습니다. + no_permission: + other: 수정권한이 없습니다. + user: + external_login_missing_user_id: + other: 서드파티 플랫폼에서 고유 사용자 ID를 제공하지 않아 로그인할 수 없습니다. 웹사이트 관리자에게 문의하세요. + external_login_unbinding_forbidden: + other: 이 로그인을 제거하기 전에 계정에 로그인 비밀번호를 설정하세요. + email_or_password_wrong: + other: + other: 이메일 또는 비밀번호가 일치하지 않습니다. + not_found: + other: 사용자를 찾을 수 없습니다. + suspended: + other: 사용자가 정지되었습니다. + username_invalid: + other: 유효하지 않은 이름입니다. + username_duplicate: + other: 이미 사용중인 이름입니다. + set_avatar: + other: 아바타 설정에 실패했습니다. + cannot_update_your_role: + other: 수정할 수 없습니다. + not_allowed_registration: + other: 현재 사이트는 등록할 수 없습니다. + not_allowed_login_via_password: + other: 현재 사이트는 비밀번호를 통해 로그인할 수 없습니다. + access_denied: + other: 접근이 거부당했습니다. + page_access_denied: + other: 이 페이지에 대한 접근 권한이 없습니다. + add_bulk_users_format_error: + other: "줄 {{.Line}}의 '{{.Content}}' 근처 {{.Field}} 형식 오류입니다. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "한 번에 추가할 수 있는 사용자 수는 1-{{.MaxAmount}} 범위 내에 있어야 합니다." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: 컨피그파일 읽기를 실패했습니다 + database: + connection_failed: + other: 데이터베이스 연결을 실패했습니다 + create_table_failed: + other: 테이블 생성을 실패했습니다 + install: + create_config_failed: + other: config.yaml 파일을 생성할 수 없습니다. + upload: + unsupported_file_format: + other: 지원하지 않는 파일 형식입니다. + site_info: + config_not_found: + other: 사이트 설정을 찾을 수 없습니다. + badge: + object_not_found: + other: 배지 객체를 찾을 수 없습니다 + reason: + spam: + name: + other: 스팸 + desc: + other: 이 게시물은 광고로 인식되었습니다. 현재 주제와 관련이 없습니다. + rude_or_abusive: + name: + other: 폭언 또는 무례한 언행입니다. + desc: + other: "대화에 부적절한 내용입니다." + a_duplicate: + name: + other: 중복 + desc: + other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다. + placeholder: + other: 이미 존재하는 질문입니다 + not_a_answer: + name: + other: 답변이 아닙니다 + desc: + other: "질문에 적절한 대답이 아닙니다. 편집, 댓글, 다른 질문, 또는 완전히 삭제된 것이어야 합니다." + no_longer_needed: + name: + other: 더 이상 필요하지 않습니다. + desc: + other: 이 의견은 구식이거나 대화 중이거나 이 게시물과 관련이 없습니다. + something: + name: + other: 다른 항목 + desc: + other: 이 게시물은 위에 나열되지 않은 다른 이유로 직원의 주의가 필요합니다. + placeholder: + other: 귀하가 우려하는 사항을 구체적으로 알려주세요. + community_specific: + name: + other: 커뮤니티별 특정 이유 + desc: + other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다. + not_clarity: + name: + other: 세부사항 또는 명확성이 필요합니다. + desc: + other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다. + looks_ok: + name: + other: 괜찮아 보입니다. + desc: + other: 이 게시물은 괜찮으며 품질이 낮지 않습니다. + needs_edit: + name: + other: 편집이 필요하고, 제가 했습니다. + desc: + other: 이 게시물에 대한 문제를 직접 개선하고 수정합니다. + needs_close: + name: + other: 닫혀야 함 + desc: + other: 닫힌 질문은 대답할 수 없지만 편집, 투표 및 댓글 작성을 할 수 있습니다. + needs_delete: + name: + other: 삭제해야 함 + desc: + other: 이 게시물은 삭제되었습니다. + question: + close: + duplicate: + name: + other: 스팸 + desc: + other: 이 질문은 이전에 질문한 적이 있으며 이미 답변이 있습니다. + guideline: + name: + other: 커뮤니티 특정 이유 + desc: + other: 이 질문은 커뮤니티 가이드라인에 부합하지 않습니다. + multiple: + name: + other: 세부사항 또는 명확성이 필요합니다. + desc: + other: 이 질문은 현재 하나에 여러 개의 질문이 포함되어 있습니다. 하나의 문제에만 초점을 맞추어야 합니다. + other: + name: + other: 다른 이유 + desc: + other: 이 게시물에는 위에 나열되지 않은 다른 이유가 필요합니다. + operation_type: + asked: + other: 질문됨 + answered: + other: 답변됨 + modified: + other: 수정됨 + deleted_title: + other: 이미 삭제된 게시물입니다. + questions_title: + other: 질문들 + tag: + tags_title: + other: 태그 + no_description: + other: 이 태그에는 설명이 없습니다. + notification: + action: + update_question: + other: 수정된 질문 + answer_the_question: + other: 대답한 질문 + update_answer: + other: 수정된 대답 + accept_answer: + other: 채택된 답변 + comment_question: + other: 질문에 댓글 달림 + comment_answer: + other: 답변에 댓글 달림 + reply_to_you: + other: 당신에게 답변함 + mention_you: + other: 당신을 언급함 + your_question_is_closed: + other: 당신의 질문이 닫혔습니다 + your_question_was_deleted: + other: 당신의 질문이 삭제되었습니다 + your_answer_was_deleted: + other: 당신의 답변이 삭제되었습니다 + your_comment_was_deleted: + other: 당신의 댓글이 삭제되었습니다 + up_voted_question: + other: 질문에 추천함 + down_voted_question: + other: 질문에 비추천함 + up_voted_answer: + other: 답변에 추천함 + down_voted_answer: + other: 답변에 비추천함 + up_voted_comment: + other: 댓글에 추천함 + invited_you_to_answer: + other: 답변에 초대함 + earned_badge: + other: '"{{.BadgeName}}" 배지를 획득했습니다' + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] 새 이메일 주소 확인" + body: + other: "다음 링크를 클릭하여 {{.SiteName}} 의 새 이메일 주소를 확인하세요:
\n{{.ChangeEmailUrl}}

\n\n이 변경을 요청하지 않았다면 이 이메일을 무시하세요.

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 님이 답변을 작성했습니다" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 님이 답변을 요청했습니다" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
답변을 아실 것 같습니다.

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 님이 당신의 게시물에 댓글을 남겼습니다" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n{{.SiteName}} 에서 보기

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" + new_question: + title: + other: "[{{.SiteName}}] 새 질문: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요.

\n\n구독 취소" + pass_reset: + title: + other: "[{{.SiteName }}] 비밀번호 재설정" + body: + other: "누군가가 {{.SiteName}} 에서 당신의 비밀번호 재설정을 요청했습니다.

\n\n당신이 요청하지 않았다면 이 이메일을 무시해도 됩니다.

\n\n새 비밀번호를 선택하려면 다음 링크를 클릭하세요:
\n{{.PassResetUrl}}\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." + register: + title: + other: "[{{.SiteName}}] 새 계정 확인" + body: + other: "{{.SiteName}} 에 오신 것을 환영합니다!

\n\n새 계정을 확인하고 활성화하려면 다음 링크를 클릭하세요:
\n{{.RegisterUrl}}

\n\n위 링크가 클릭되지 않으면 웹 브라우저의 주소창에 복사하여 붙여넣어 보세요.\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." + test: + title: + other: "[{{.SiteName}}] 테스트 이메일" + body: + other: "이것은 테스트 이메일입니다.\n

\n\n--
\n참고: 이것은 자동 시스템 이메일입니다. 응답해도 확인되지 않으므로 이 메시지에 회신하지 마세요." + action_activity_type: + upvote: + other: 추천 + upvoted: + other: 추천함 + downvote: + other: 비추천 + downvoted: + other: 비추천함 + accept: + other: 채택 + accepted: + other: 채택됨 + edit: + other: 수정 + review: + queued_post: + other: 대기 중인 게시물 + flagged_post: + other: 신고된 게시물 + suggested_post_edit: + other: 건의된 수정 + reaction: + tooltip: + other: "{{ .Names }} 외 {{ .Count }} 명..." + badge: + default_badges: + autobiographer: + name: + other: 자서전 작가 + desc: + other: 프로필 정보를 작성했습니다. + certified: + name: + other: 인증됨 + desc: + other: 신규 사용자 튜토리얼을 완료했습니다. + editor: + name: + other: 에디터 + desc: + other: 첫 게시물 편집. + first_flag: + name: + other: 첫 번째 플래그 + desc: + other: 첫 번째로 게시물에 플래그를 설정했습니다. + first_upvote: + name: + other: 첫 번째 추천 + desc: + other: 첫 번째로 게시물을 추천했습니다. + first_link: + name: + other: 첫 번째 링크 + desc: + other: 첫 번째로 다른 게시물에 링크를 추가했습니다. + first_reaction: + name: + other: 첫 번째 반응 + desc: + other: 첫 번째로 게시물에 반응했습니다. + first_share: + name: + other: 첫 번째 공유 + desc: + other: 첫 번째로 게시물을 공유했습니다. + scholar: + name: + other: 학자 + desc: + other: 질문을 하고 답변을 채택했습니다. + commentator: + name: + other: 해설자 + desc: + other: 댓글 5 개 남기기. + new_user_of_the_month: + name: + other: 이달의 신규 사용자 + desc: + other: 첫 달의 뛰어난 기여. + read_guidelines: + name: + other: 가이드라인 읽기 + desc: + other: '[커뮤니티 가이드라인] 을 읽어보세요.' + reader: + name: + other: 리더 + desc: + other: 10 개 이상의 답변이 있는 주제의 모든 답변을 읽어보세요. + welcome: + name: + other: 환영합니다 + desc: + other: 추천을 받았습니다. + nice_share: + name: + other: 좋은 공유 + desc: + other: 25 명의 고유 방문자와 게시물을 공유했습니다. + good_share: + name: + other: 더 좋은 공유 + desc: + other: 300 명의 고유 방문자와 게시물을 공유했습니다. + great_share: + name: + other: 훌륭한 공유 + desc: + other: 1000 명의 고유 방문자와 게시물을 공유했습니다. + out_of_love: + name: + other: 사랑이 식어서 + desc: + other: 하루에 50 개의 추천을 사용했습니다. + higher_love: + name: + other: 더 큰 사랑 + desc: + other: 하루에 50 개의 추천을 5 번 사용했습니다. + crazy_in_love: + name: + other: 사랑에 미치다 + desc: + other: 하루에 50 개의 추천을 20 번 사용했습니다. + promoter: + name: + other: 홍보자 + desc: + other: 사용자를 초대했습니다. + campaigner: + name: + other: 캠페인 담당자 + desc: + other: 3 명의 기본 사용자를 초대했습니다. + champion: + name: + other: 챔피언 + desc: + other: 5 명의 멤버를 초대했습니다. + thank_you: + name: + other: 감사합니다 + desc: + other: 20 개의 추천받은 게시물을 보유하고 10 개의 추천을 주었습니다. + gives_back: + name: + other: 보답 + desc: + other: 100 개의 추천받은 게시물을 보유하고 100 개의 추천을 주었습니다. + empathetic: + name: + other: 공감적 + desc: + other: 500 개의 추천받은 게시물을 보유하고 1000 개의 추천을 주었습니다. + enthusiast: + name: + other: 열성팬 + desc: + other: 10 일 연속으로 방문했습니다. + aficionado: + name: + other: 애호가 + desc: + other: 100 일 연속으로 방문했습니다. + devotee: + name: + other: 열성 지지자 + desc: + other: 365 일 연속으로 방문했습니다. + anniversary: + name: + other: 기념일 + desc: + other: 1 년 동안 활발한 멤버로 활동하며 최소 한 번 이상 게시했습니다. + appreciated: + name: + other: 감사합니다 + desc: + other: 20 개의 게시물에서 1 번의 추천을 받았습니다. + respected: + name: + other: 존경받는 + desc: + other: 100 개의 게시물에서 2 번의 추천을 받았습니다. + admired: + name: + other: 존경받는 + desc: + other: 300 개의 게시물에서 5 번의 추천을 받았습니다. + solved: + name: + other: 해결됨 + desc: + other: 답변이 채택되었습니다. + guidance_counsellor: + name: + other: 지도 상담사 + desc: + other: 10 개의 답변이 채택되었습니다. + know_it_all: + name: + other: 만물박사 + desc: + other: 50 개의 답변이 채택되었습니다. + solution_institution: + name: + other: 솔루션 기관 + desc: + other: 150 개의 답변이 채택되었습니다. + nice_answer: + name: + other: 좋은 답변 + desc: + other: 답변 점수가 10 점 이상입니다. + good_answer: + name: + other: 더 좋은 답변 + desc: + other: 답변 점수가 25 점 이상입니다. + great_answer: + name: + other: 훌륭한 답변 + desc: + other: 답변 점수가 50 점 이상입니다. + nice_question: + name: + other: 좋은 질문 + desc: + other: 질문 점수가 10 점 이상입니다. + good_question: + name: + other: 좋은 질문 + desc: + other: 질문 점수가 25 점 이상입니다. + great_question: + name: + other: 훌륭한 질문 + desc: + other: 질문 점수가 50 점 이상입니다. + popular_question: + name: + other: 인기 질문 + desc: + other: 500 회 조회된 질문입니다. + notable_question: + name: + other: 중요 질문 + desc: + other: 1,000 회 조회된 질문입니다. + famous_question: + name: + other: 유명 질문 + desc: + other: 5,000 회 조회된 질문입니다. + popular_link: + name: + other: 인기 링크 + desc: + other: 50 번 클릭된 외부 링크를 게시했습니다. + hot_link: + name: + other: 핫 링크 + desc: + other: 300 번 클릭된 외부 링크를 게시했습니다. + famous_link: + name: + other: 유명한 링크 + desc: + other: 100 번 클릭된 외부 링크를 게시했습니다. + default_badge_groups: + getting_started: + name: + other: 시작하기 + community: + name: + other: 커뮤니티 + posting: + name: + other: 포스팅 +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 포맷 방법 + desc: >- + + pagination: + prev: 이전 + next: 다음 + page_title: + question: 질문 + questions: 질문들 + tag: 태그 + tags: 태그들 + tag_wiki: 태그 위키 + create_tag: 태그 생성 + edit_tag: 태그 수정 + ask_a_question: Create Question + edit_question: 질문 수정 + edit_answer: 답변 수정 + search: 검색 + posts_containing: 포함된 게시물 + settings: 설정 + notifications: 알림 + login: 로그인 + sign_up: 회원 가입 + account_recovery: 계정 복구 + account_activation: 계정 활성화 + confirm_email: 이메일 확인 + account_suspended: 계정 정지 + admin: 관리자 + change_email: 이메일 수정 + install: 답변 설치 + upgrade: 답변 업그레이드 + maintenance: 웹사이트 유지보수 + users: 사용자 + oauth_callback: 처리 중 + http_404: HTTP 오류 404 + http_50X: HTTP 오류 500 + http_403: HTTP 오류 403 + logout: 로그아웃 + notifications: + title: 알림 + inbox: 받은 편지함 + achievement: 업적 + new_alerts: 새로운 알림 + all_read: 모두 읽음 처리 + show_more: 더 보기 + someone: 누군가 + inbox_type: + all: 전체 + posts: 게시물 + invites: 초대 + votes: 투표 + answer: 답변 + question: 질문 + badge_award: 뱃지 + suspended: + title: 계정이 정지되었습니다 + until_time: "당신의 계정은 {{ time }}까지 정지되었습니다." + forever: 이 사용자는 영구 정지되었습니다. + end: 커뮤니티 가이드라인을 준수하지 않았습니다. + contact_us: 문의하기 + editor: + blockquote: + text: 인용구 + bold: + text: 강조 + chart: + text: 차트 + flow_chart: 플로우 차트 + sequence_diagram: 시퀀스 다이어그램 + class_diagram: 클래스 다이어그램 + state_diagram: 상태 다이어그램 + entity_relationship_diagram: 엔터티 관계 다이어그램 + user_defined_diagram: 사용자 정의 다이어그램 + gantt_chart: 간트 차트 + pie_chart: 파이 차트 + code: + text: 코드 예시 + add_code: 코드 예시 추가 + form: + fields: + code: + label: 코드 + msg: + empty: 코드를 입력하세요. + language: + label: 언어 + placeholder: 자동 감지 + btn_cancel: 취소 + btn_confirm: 추가 + formula: + text: 수식 + options: + inline: 인라인 수식 + block: 블록 수식 + heading: + text: 제목 + options: + h1: 제목 1 + h2: 제목 2 + h3: 제목 3 + h4: 제목 4 + h5: 제목 5 + h6: 제목 6 + help: + text: 도움말 + hr: + text: 가로규칙 + image: + text: 이미지 + add_image: 이미지 추가 + tab_image: 이미지 업로드 + form_image: + fields: + file: + label: 이미지 파일 + btn: 이미지 선택 + msg: + empty: 파일을 선택하세요. + only_image: 이미지 파일만 허용됩니다. + max_size: 파일 크기는 {{size}} MB 를 초과할 수 없습니다. + desc: + label: 설명 + tab_url: 이미지 URL + form_url: + fields: + url: + label: 이미지 URL + msg: + empty: 이미지 URL을 입력하세요. + name: + label: 설명 + btn_cancel: 취소 + btn_confirm: 추가 + uploading: 업로드 중 + indent: + text: 들여쓰기 + outdent: + text: 내어쓰기 + italic: + text: 이탤릭체 + link: + text: 링크 + add_link: 링크 추가 + form: + fields: + url: + label: URL + msg: + empty: URL을 입력하세요. + name: + label: 설명 + btn_cancel: 취소 + btn_confirm: 추가 + ordered_list: + text: 번호 매긴 목록 + unordered_list: + text: 글머리 기호 목록 + table: + text: 표 + heading: 제목 + cell: 셀 + file: + text: 파일 첨부 + not_supported: "해당 파일 형식을 지원하지 않습니다. {{file_type}} 로 다시 시도해 주세요." + max_size: "첨부 파일 크기는 {{size}} MB 를 초과할 수 없습니다." + close_modal: + title: 이 게시물을 다음과 같은 이유로 닫습니다... + btn_cancel: 취소 + btn_submit: 제출 + remark: + empty: 비어 있을 수 없습니다. + msg: + empty: 이유를 선택해 주세요. + report_modal: + flag_title: 이 게시물을 신고합니다... + close_title: 이 게시물을 다음과 같은 이유로 닫습니다... + review_question_title: 질문 검토 + review_answer_title: 답변 검토 + review_comment_title: 댓글 검토 + btn_cancel: 취소 + btn_submit: 제출 + remark: + empty: 비어 있을 수 없습니다. + msg: + empty: 이유를 선택해 주세요. + not_a_url: URL 형식이 올바르지 않습니다. + url_not_match: URL 원본이 현재 웹사이트와 일치하지 않습니다. + tag_modal: + title: 새로운 태그 생성 + form: + fields: + display_name: + label: 표시 이름 + msg: + empty: 표시 이름을 입력하세요. + range: 표시 이름은 최대 35자까지 입력 가능합니다. + slug_name: + label: URL 슬러그 + desc: '"a-z", "0-9", "+ # - ." 문자 집합을 사용해야 합니다.' + msg: + empty: URL 슬러그를 입력하세요. + range: URL 슬러그는 최대 35자까지 입력 가능합니다. + character: 허용되지 않은 문자 집합이 포함되어 있습니다.' + desc: + label: 설명 + revision: + label: 개정 + edit_summary: + label: 편집 요약 + placeholder: >- + 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) + btn_cancel: 취소 + btn_submit: 제출 + btn_post: 새 태그 게시 + tag_info: + created_at: 생성됨 + edited_at: 편집됨 + history: 히스토리 + synonyms: + title: 동의어 + text: 다음 태그가 다음으로 다시 매핑됩니다 + empty: 동의어가 없습니다. + btn_add: 동의어 추가 + btn_edit: 편집 + btn_save: 저장 + synonyms_text: 다음 태그가 다음으로 다시 매핑됩니다 + delete: + title: 이 태그 삭제 + tip_with_posts: >- +

게시물이 있는 태그 삭제는 허용되지 않습니다.

먼저 게시물에서 이 태그를 제거해 주세요.

+ tip_with_synonyms: >- +

동의어가 있는 태그 삭제는 허용되지 않습니다.

먼저 이 태그에서 동의어를 제거해 주세요.

+ tip: 정말로 삭제하시겠습니까? + close: 닫기 + merge: + title: 태그 병합 + source_tag_title: 원본 태그 + source_tag_description: 원본 태그와 관련 데이터가 대상 태그로 재매핑됩니다. + target_tag_title: 대상 태그 + target_tag_description: 병합 후 이 두 태그 간 동의어가 생성됩니다. + no_results: 일치하는 태그가 없습니다 + btn_submit: 제출 + btn_close: 닫기 + edit_tag: + title: 태그 수정 + default_reason: 태그 수정 + default_first_reason: 태그 추가 + btn_save_edits: 수정 저장 + btn_cancel: 취소 + dates: + long_date: MMM D + long_date_with_year: "YYYY년 M월 D일" + long_date_with_time: "YYYY년 MMM D일 HH:mm" + now: 방금 전 + x_seconds_ago: "{{count}}초 전" + x_minutes_ago: "{{count}}분 전" + x_hours_ago: "{{count}}시간 전" + hour: 시간 + day: 일 + hours: 시간 + days: 일 + month: month + months: months + year: year + reaction: + heart: 하트 + smile: 스마일 + frown: 찡그린 표정 + btn_label: 반응 추가 또는 제거 + undo_emoji: '{{ emoji }} 반응 취소' + react_emoji: '{{ emoji }} 로 반응' + unreact_emoji: '{{ emoji }} 반응 취소' + comment: + btn_add_comment: 댓글 추가 + reply_to: 답글 달기 + btn_reply: 답글 + btn_edit: 수정 + btn_delete: 삭제 + btn_flag: 신고 + btn_save_edits: 수정 저장 + btn_cancel: 취소 + show_more: "{{count}}개의 댓글 더 보기" + tip_question: >- + 더 많은 정보를 요청하거나 개선을 제안하기 위해 댓글을 사용하세요. 댓글에서 질문에 답변하지는 마세요. + tip_answer: >- + 다른 사용자에게 답변하거나 변경 사항을 알릴 때 댓글을 사용하세요. 새로운 정보를 추가하는 경우에는 게시물을 수정하세요. + tip_vote: 게시물에 유용한 정보를 추가합니다. + edit_answer: + title: 답변 수정 + default_reason: 답변 수정 + default_first_reason: 답변 추가 + form: + fields: + revision: + label: 개정 + answer: + label: 답변 + feedback: + characters: 내용은 최소 6자 이상이어야 합니다. + edit_summary: + label: 편집 요약 + placeholder: >- + 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) + btn_save_edits: 수정 저장 + btn_cancel: 취소 + tags: + title: 태그들 + sort_buttons: + popular: 인기순 + name: 이름 + newest: 최신순 + button_follow: 팔로우 + button_following: 팔로잉 중 + tag_label: 질문들 + search_placeholder: 태그 이름으로 필터링 + no_desc: 이 태그에는 설명이 없습니다. + more: 더 보기 + wiki: 위키 + ask: + title: Create Question + edit_title: 질문 수정 + default_reason: 질문 수정 + default_first_reason: Create question + similar_questions: 유사한 질문 + form: + fields: + revision: + label: 개정 + title: + label: 제목 + placeholder: What's your topic? Be specific. + msg: + empty: 제목을 입력하세요. + range: 제목은 최대 150자까지 입력 가능합니다. + body: + label: 본문 + msg: + empty: 본문을 입력하세요. + tags: + label: 태그 + msg: + empty: 태그를 입력하세요. + answer: + label: 답변 + msg: + empty: 답변을 입력하세요. + edit_summary: + label: 편집 요약 + placeholder: >- + 수정 사항을 간략히 설명하세요 (철자 수정, 문법 수정, 서식 개선 등) + btn_post_question: 질문 게시하기 + btn_save_edits: 수정사항 저장 + answer_question: 질문에 대한 답변 작성 + post_question&answer: 질문과 답변 게시하기 + tag_selector: + add_btn: 태그 추가 + create_btn: 새 태그 생성 + search_tag: 태그 검색 + hint: "Describe what your content is about, at least one tag is required." + no_result: 일치하는 태그가 없습니다. + tag_required_text: 필수 태그 (적어도 하나) + header: + nav: + question: 질문 + tag: 태그 + user: 사용자 + badges: 뱃지 + profile: 프로필 + setting: 설정 + logout: 로그아웃 + admin: 관리자 + review: 리뷰 + bookmark: 즐겨찾기 + moderation: 운영 + search: + placeholder: 검색 + footer: + build_on: >- + Powered by <1> Apache Answer - Q&A 커뮤니티를 지원하는 오픈 소스 소프트웨어입니다.
Made with love © {{cc}}. + upload_img: + name: 변경 + loading: 로딩 중... + pic_auth_code: + title: 캡차 + placeholder: 위의 텍스트를 입력하세요 + msg: + empty: 캡차를 입력하세요. + inactive: + first: >- + 거의 다 되었습니다! {{mail}}로 활성화 메일을 보냈습니다. 계정을 활성화하려면 메일 안의 지침을 따르세요. + info: "메일이 도착하지 않았다면, 스팸 메일함도 확인해 주세요." + another: >- + {{mail}}로 또 다른 활성화 이메일을 보냈습니다. 메일이 도착하는 데 몇 분 정도 걸릴 수 있으니 스팸 메일함도 확인해 주세요. + btn_name: 활성화 이메일 재전송 + change_btn_name: 이메일 변경 + msg: + empty: 비어 있을 수 없습니다. + resend_email: + url_label: 활성화 이메일을 재전송하시겠습니까? + url_text: 위의 활성화 링크를 사용자에게 제공할 수도 있습니다. + login: + login_to_continue: 계속하려면 로그인하세요 + info_sign: 계정이 없으신가요? <1>가입하기 + info_login: 이미 계정이 있으신가요? <1>로그인하기 + agreements: 가입하면 <1>개인정보 보호 정책과 <3>이용 약관에 동의하게 됩니다. + forgot_pass: 비밀번호를 잊으셨나요? + name: + label: 이름 + msg: + empty: 이름을 입력하세요. + range: 이름은 2 자에서 30 자 사이여야 합니다. + character: '문자 집합 "a-z", "0-9", " - . _"를 사용해야 합니다' + email: + label: 이메일 + msg: + empty: 이메일을 입력하세요. + password: + label: 비밀번호 + msg: + empty: 비밀번호를 입력하세요. + different: 입력된 비밀번호가 일치하지 않습니다. + account_forgot: + page_title: 비밀번호를 잊으셨나요? + btn_name: 비밀번호 재설정 이메일 보내기 + send_success: >- + {{mail}}에 해당하는 계정이 있다면 곧 비밀번호 재설정 방법을 안내하는 이메일을 받으실 수 있습니다. + email: + label: 이메일 + msg: + empty: 이메일을 입력하세요. + change_email: + btn_cancel: 취소 + btn_update: 이메일 주소 업데이트 + send_success: >- + {{mail}}에 해당하는 계정이 있다면 곧 이메일 주소 변경 방법을 안내하는 이메일을 받으실 수 있습니다. + email: + label: 새 이메일 + msg: + empty: 이메일을 입력하세요. + oauth: + connect: '{{ auth_name }}로 연결' + remove: '{{ auth_name }} 연결 해제' + oauth_bind_email: + subtitle: 계정에 복구 이메일 추가 + btn_update: 이메일 주소 업데이트 + email: + label: 이메일 + msg: + empty: 이메일을 입력하세요. + modal_title: 이미 등록된 이메일 + modal_content: 이 이메일 주소는 이미 등록되어 있습니다. 기존 계정에 연결하시겠습니까? + modal_cancel: 이메일 변경 + modal_confirm: 기존 계정에 연결하기 + password_reset: + page_title: 비밀번호 재설정 + btn_name: 비밀번호 재설정 + reset_success: >- + 비밀번호가 성공적으로 변경되었습니다. 로그인 페이지로 이동합니다. + link_invalid: >- + 죄송합니다. 이 비밀번호 재설정 링크는 더 이상 유효하지 않습니다. 이미 비밀번호를 재설정하셨을 수 있습니다. + to_login: 로그인 페이지로 이동 + password: + label: 비밀번호 + msg: + empty: 비밀번호를 입력하세요. + length: 비밀번호는 8자에서 32자 사이여야 합니다. + different: 입력한 비밀번호가 일치하지 않습니다. + password_confirm: + label: 새 비밀번호 확인 + settings: + page_title: 설정 + goto_modify: 수정으로 이동 + nav: + profile: 프로필 + notification: 알림 + account: 계정 + interface: 인터페이스 + profile: + heading: 프로필 + btn_name: 저장 + display_name: + label: 표시 이름 + msg: 표시 이름을 입력하세요. + msg_range: 표시 이름은 2-30 자 길이여야 합니다. + username: + label: 사용자 이름 + caption: 다른 사용자가 "@사용자이름"으로 멘션할 수 있습니다. + msg: 사용자 이름을 입력하세요. + msg_range: 유저 이름은 2-30 자 길이여야 합니다. + character: '문자 집합 "a-z", "0-9", " - . _"을 사용해야 합니다.' + avatar: + label: 프로필 이미지 + gravatar: Gravatar + gravatar_text: Gravatar에서 이미지를 변경할 수 있습니다. + custom: 사용자 정의 + custom_text: 사용자 이미지를 업로드할 수 있습니다. + default: 시스템 기본 이미지 + msg: 프로필 이미지를 업로드하세요. + bio: + label: 자기 소개 + website: + label: 웹사이트 + placeholder: "https://example.com" + msg: 웹사이트 형식이 올바르지 않습니다. + location: + label: 위치 + placeholder: "도시, 국가" + notification: + heading: 이메일 알림 + turn_on: 켜기 + inbox: + label: 받은 편지함 알림 + description: 질문에 대한 답변, 댓글, 초대 등을 받습니다. + all_new_question: + label: 모든 새 질문 + description: 모든 새 질문에 대해 알림을 받습니다. 주당 최대 50개의 질문까지. + all_new_question_for_following_tags: + label: 팔로우 태그의 모든 새 질문 + description: 팔로우하는 태그의 새로운 질문에 대해 알림을 받습니다. + account: + heading: 계정 + change_email_btn: 이메일 변경 + change_pass_btn: 비밀번호 변경 + change_email_info: >- + 해당 주소로 이메일을 보냈습니다. 확인 지침을 따라주세요. + email: + label: 이메일 + new_email: + label: 새 이메일 + msg: 새 이메일을 입력하세요. + pass: + label: 현재 비밀번호 + msg: 비밀번호를 입력하세요. + password_title: 비밀번호 + current_pass: + label: 현재 비밀번호 + msg: + empty: 현재 비밀번호를 입력하세요. + length: 비밀번호는 8자에서 32자 사이여야 합니다. + different: 입력한 두 비밀번호가 일치하지 않습니다. + new_pass: + label: 새 비밀번호 + pass_confirm: + label: 새 비밀번호 확인 + interface: + heading: 인터페이스 + lang: + label: 인터페이스 언어 + text: 사용자 인터페이스 언어입니다. 페이지를 새로고침하면 변경됩니다. + my_logins: + title: 내 로그인 정보 + label: 이 사이트에서 이 계정으로 로그인하거나 가입하세요. + modal_title: 로그인 제거 + modal_content: 이 계정에서 이 로그인을 제거하시겠습니까? + modal_confirm_btn: 제거 + remove_success: 제거되었습니다. + toast: + update: 업데이트 성공 + update_password: 비밀번호가 성공적으로 변경되었습니다. + flag_success: 신고 감사합니다. + forbidden_operate_self: 자신에 대한 작업은 금지되어 있습니다. + review: 검토 후에 귀하의 수정 사항이 표시됩니다. + sent_success: 전송 성공 + related_question: + title: Related + answers: 답변 + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: 질문자 초대 + desc: 답변을 알고 있을 것으로 생각되는 사람을 선택하세요. + invite: 답변 초대 + add: 사람 추가 + search: 사람 검색 + question_detail: + action: 동작 + Asked: 질문함 + asked: 질문 작성 + update: 수정됨 + edit: 편집됨 + commented: 댓글 작성 + Views: 조회수 + Follow: 팔로우 + Following: 팔로잉 중 + follow_tip: 이 질문을 팔로우하여 알림을 받으세요. + answered: 답변 작성 + closed_in: 답변 종료 + show_exist: 기존 질문 표시 + useful: 유용함 + question_useful: 유용하고 명확함 + question_un_useful: 불명확하거나 유용하지 않음 + question_bookmark: 이 질문 즐겨찾기 + answer_useful: 유용함 + answer_un_useful: 유용하지 않음 + answers: + title: 답변 + score: 점수 + newest: 최신순 + oldest: 오래된 순 + btn_accept: 채택 + btn_accepted: 채택됨 + write_answer: + title: 당신의 답변 + edit_answer: 내 답변 편집하기 + btn_name: 답변 게시하기 + add_another_answer: 다른 답변 추가 + confirm_title: 답변 계속하기 + continue: 계속 + confirm_info: >- +

다른 답변을 추가하시겠습니까?

대신 기존 답변을 향상시키고 개선할 수 있는 수정 링크를 사용할 수 있습니다.

+ empty: 답변을 입력해주세요. + characters: 내용은 최소 6자 이상이어야 합니다. + tips: + header_1: 답변해 주셔서 감사합니다 + li1_1: 질문에 답변을 제공하세요. 세부 사항을 설명하고 연구 결과를 공유하세요. + li1_2: 발언을 뒷받침하는 자료나 개인적인 경험을 통해 주장을 뒷받침하세요. + header_2: 하지만 피해야 할 것들 ... + li2_1: 도움을 요청하거나 해명을 구하거나 다른 답변에 응답하는 것. + reopen: + confirm_btn: 다시 열기 + title: 이 게시물 다시 열기 + content: 정말 다시 열기를 원하시나요? + list: + confirm_btn: 목록 + title: 이 게시물 목록에 추가하기 + content: 정말 목록에 추가하시겠습니까? + unlist: + confirm_btn: 목록 해제 + title: 이 게시물 목록에서 제외하기 + content: 정말 목록에서 제외하시겠습니까? + pin: + title: 이 게시물 고정하기 + content: 글로벌로 고정하시겠습니까? 이 게시물은 모든 게시물 목록 상단에 표시됩니다. + confirm_btn: 고정하기 + delete: + title: 이 게시물 삭제하기 + question: >- +

답변이 있는 질문을 삭제하는 것은 권장하지 않습니다 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다.

답변이 있는 질문을 반복적으로 삭제하는 경우 질문 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까? + answer_accepted: >- +

채택된 답변을 삭제하는 것은 권장하지 않습니다 이는 이 지식을 필요로 하는 사용자에게 정보를 제공하지 못하게 될 수 있습니다.

채택된 답변을 반복적으로 삭제하는 경우 답변 권한이 차단될 수 있습니다. 정말 삭제하시겠습니까? + other: 정말 삭제하시겠습니까? + tip_answer_deleted: 이 답변은 삭제되었습니다. + undelete_title: 이 게시물 복구하기 + undelete_desc: 정말 복구하시겠습니까? + btns: + confirm: 확인 + cancel: 취소 + edit: 편집 + save: 저장 + delete: 삭제 + undelete: 복구 + list: 목록 + unlist: 목록 해제 + unlisted: 목록에서 해제됨 + login: 로그인 + signup: 가입하기 + logout: 로그아웃 + verify: 확인 + create: 생성 + approve: 승인 + reject: 거부 + skip: 건너뛰기 + discard_draft: 임시 저장 삭제 + pinned: 고정됨 + all: 모두 + question: 질문 + answer: 답변 + comment: 댓글 + refresh: 새로 고침 + resend: 재전송 + deactivate: 비활성화 + active: 활성화 + suspend: 정지 + unsuspend: 정지 해제 + close: 닫기 + reopen: 다시 열기 + ok: 확인 + light: 밝게 + dark: 어둡게 + system_setting: 시스템 설정 + default: 기본 + reset: 재설정 + tag: 태그 + post_lowercase: 게시물 + filter: 필터 + ignore: 무시 + submit: 제출 + normal: 일반 + closed: 닫힘 + deleted: 삭제됨 + deleted_permanently: 영구 삭제 + pending: 보류 중 + more: 더 보기 + view: 보기 + card: 카드 + compact: 간단히 + display_below: 아래에 표시 + always_display: 항상 표시 + or: 또는 + back_sites: 사이트로 돌아가기 + search: + title: 검색 결과 + keywords: 키워드 + options: 옵션 + follow: 팔로우 + following: 팔로잉 중 + counts: "{{count}} 개의 결과" + counts_loading: "... Results" + more: 더 보기 + sort_btns: + relevance: 관련성 + newest: 최신순 + active: 활성순 + score: 평점순 + more: 더 보기 + tips: + title: 고급 검색 팁 + tag: "<1>[태그] 태그로 검색" + user: "<1>user:사용자명 작성자로 검색" + answer: "<1>answers:0 답변이 없는 질문" + score: "<1>score:3 평점이 3 이상인 글" + question: "<1>is:question 질문만 검색" + is_answer: "<1>is:answer 답변만 검색" + empty: 아무것도 찾지 못했습니다.
다른 키워드를 사용하거나 덜 구체적인 검색을 시도하세요. + share: + name: 공유 + copy: 링크 복사 + via: 포스트 공유하기... + copied: 복사됨 + facebook: Facebook에 공유 + twitter: X에 공유하기 + cannot_vote_for_self: 자신의 글에 투표할 수 없습니다. + modal_confirm: + title: 오류... + delete_permanently: + title: 영구 삭제 + content: 영구적으로 삭제하시겠습니까? + account_result: + success: 새 계정이 확인되었습니다. 홈페이지로 이동합니다. + link: 홈페이지로 이동 + oops: 이런! + invalid: 사용하신 링크가 더 이상 작동하지 않습니다. + confirm_new_email: 이메일이 업데이트되었습니다. + confirm_new_email_invalid: >- + 죄송합니다, 이 확인 링크는 더 이상 유효하지 않습니다. 이미 이메일이 변경된 상태일 수 있습니다. + unsubscribe: + page_title: 구독 해지 + success_title: 구독 해지 완료 + success_desc: 이 구독자 목록에서 성공적으로 제거되었으며, 더 이상 우리로부터 어떠한 이메일도 받지 않게 됩니다. + link: 설정 변경하기 + question: + following_tags: 팔로우 태그 + edit: 수정 + save: 저장 + follow_tag_tip: 질문 목록을 관리하기 위해 태그를 팔로우하세요. + hot_questions: 인기 질문 + all_questions: 모든 질문 + x_questions: "{{ count }} 개의 질문" + x_answers: "{{ count }} 개의 답변" + x_posts: "{{ count }} Posts" + questions: 질문 + answers: 답변 + newest: 최신순 + active: 활성순 + hot: 인기 + frequent: 빈도 + recommend: 추천 + score: 평점순 + unanswered: 답변이 없는 질문 + modified: 수정됨 + answered: 답변됨 + asked: 질문됨 + closed: 닫힘 + follow_a_tag: 태그 팔로우하기 + more: 더 보기 + personal: + overview: 개요 + answers: 답변 + answer: 답변 + questions: 질문 + question: 질문 + bookmarks: 즐겨찾기 + reputation: 평판 + comments: 댓글 + votes: 투표 + badges: 뱃지 + newest: 최신순 + score: 평점순 + edit_profile: 프로필 수정 + visited_x_days: "{{ count }} 일 방문함" + viewed: 조회됨 + joined: 가입일 + comma: "," + last_login: 최근 접속 + about_me: 자기 소개 + about_me_empty: "// 안녕하세요, 세상아 !" + top_answers: 최고 답변 + top_questions: 최고 질문 + stats: 통계 + list_empty: 게시물을 찾을 수 없습니다.
다른 탭을 선택하실 수 있습니다. + content_empty: 게시물을 찾을 수 없습니다. + accepted: 채택됨 + answered: 답변됨 + asked: 질문됨 + downvoted: 다운투표됨 + mod_short: MOD + mod_long: 관리자 + x_reputation: 평판 + x_votes: 받은 투표 + x_answers: 답변 + x_questions: 질문 + recent_badges: 최근 배지 + install: + title: 설치 + next: 다음 + done: 완료 + config_yaml_error: config.yaml 파일을 생성할 수 없습니다. + lang: + label: 언어 선택 + db_type: + label: 데이터베이스 엔진 + db_username: + label: 사용자 이름 + placeholder: root + msg: 사용자 이름은 비워둘 수 없습니다. + db_password: + label: 비밀번호 + placeholder: root + msg: 비밀번호는 비워둘 수 없습니다. + db_host: + label: 데이터베이스 호스트 + placeholder: "db:3306" + msg: 데이터베이스 호스트는 비워둘 수 없습니다. + db_name: + label: 데이터베이스 이름 + placeholder: 답변 + msg: 데이터베이스 이름은 비워둘 수 없습니다. + db_file: + label: 데이터베이스 파일 + placeholder: /data/answer.db + msg: 데이터베이스 파일은 비워둘 수 없습니다. + ssl_enabled: + label: SSL 활성화 + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL 모드 + ssl_root_cert: + placeholder: sslrootcert 파일 경로 + msg: sslrootcert 파일 경로는 비워둘 수 없습니다 + ssl_cert: + placeholder: sslcert 파일 경로 + msg: sslcert 파일 경로는 비워둘 수 없습니다 + ssl_key: + placeholder: sslkey 파일 경로 + msg: sslkey 파일 경로는 비워둘 수 없습니다 + config_yaml: + title: config.yaml 파일 생성 + label: config.yaml 파일이 생성되었습니다. + desc: >- + config.yaml 파일을 <1>/var/wwww/xxx/ 디렉터리에 수동으로 생성하고 아래 텍스트를 붙여넣을 수 있습니다. + info: 위 작업을 완료한 후 "다음" 버튼을 클릭하세요. + site_information: 사이트 정보 + admin_account: 관리자 계정 + site_name: + label: 사이트 이름 + msg: 사이트 이름을 입력하세요. + msg_max_length: 사이트 이름은 최대 30자여야 합니다. + site_url: + label: 사이트 URL + text: 사이트의 주소입니다. + msg: + empty: 사이트 URL을 입력하세요. + incorrect: 올바른 형식의 사이트 URL을 입력하세요. + max_length: 사이트 URL은 최대 512자여야 합니다. + contact_email: + label: 연락처 이메일 + text: 이 사이트에 책임을 지는 주요 연락 이메일 주소입니다. + msg: + empty: 연락처 이메일을 입력하세요. + incorrect: 올바른 형식의 연락처 이메일을 입력하세요. + login_required: + label: 비공개 + switch: 로그인 필요 + text: 로그인한 사용자만 이 커뮤니티에 접근할 수 있습니다. + admin_name: + label: 이름 + msg: 이름을 입력하세요. + character: '"a-z", "A-Z", "0-9", " - . _" 문자 집합을 사용해야 합니다' + msg_max_length: 이름은 2 자 이상 30 자 이하여야 합니다. + admin_password: + label: 비밀번호 + text: >- + 로그인에 필요한 비밀번호입니다. 안전한 위치에 보관하세요. + msg: 비밀번호를 입력하세요. + msg_min_length: 비밀번호는 최소 8자여야 합니다. + msg_max_length: 비밀번호는 최대 32자여야 합니다. + admin_confirm_password: + label: "비밀번호 확인" + text: "확인을 위해 비밀번호를 다시 입력해주세요." + msg: "비밀번호 확인이 일치하지 않습니다." + admin_email: + label: 이메일 + text: 로그인에 필요한 이메일입니다. + msg: + empty: 이메일을 입력하세요. + incorrect: 올바른 형식의 이메일을 입력하세요. + ready_title: 귀하의 사이트가 준비되었습니다 + ready_desc: >- + 추가 설정을 원하시면 <1>관리자 섹션에서 찾아보세요; 사이트 메뉴에서 확인할 수 있습니다. + good_luck: "재미있고 행운을 빕니다!" + warn_title: 경고 + warn_desc: >- + 파일 <1>config.yaml이 이미 존재합니다. 이 파일의 구성 항목 중 재설정이 필요하면 먼저 삭제하세요. + install_now: <1>지금 설치해보세요. + installed: 이미 설치됨 + installed_desc: >- + 이미 설치된 것으로 보입니다. 재설치하려면 먼저 이전 데이터베이스 테이블을 삭제하세요. + db_failed: 데이터베이스 연결 실패 + db_failed_desc: >- + <1>config.yaml 파일에 있는 데이터베이스 정보가 올바르지 않거나 데이터베이스 서버와 연결할 수 없습니다. 호스트의 데이터베이스 서버가 다운된 경우입니다. + counts: + views: 조회수 + votes: 투표 + answers: 답변 + accepted: 채택됨 + page_error: + http_error: HTTP 오류 {{ code }} + desc_403: 이 페이지에 접근할 권한이 없습니다. + desc_404: 죄송합니다. 이 페이지는 존재하지 않습니다. + desc_50X: 서버에서 오류가 발생하여 요청을 완료할 수 없습니다. + back_home: 홈페이지로 돌아가기 + page_maintenance: + desc: "저희는 현재 유지보수 중입니다. 곧 돌아오겠습니다." + nav_menus: + dashboard: 대시보드 + contents: 콘텐츠 + questions: 질문 + answers: 답변 + users: 사용자 + badges: 뱃지 + flags: 신고하기 + settings: 설정 + general: 일반 + interface: 인터페이스 + smtp: SMTP + branding: 브랜딩 + legal: 법적 사항 + write: 글 작성 + tos: 이용 약관 + privacy: 개인정보 보호 + seo: 검색 엔진 최적화 + customize: 사용자 정의 + themes: 테마 + login: 로그인 + privileges: 권한 + plugins: 플러그인 + installed_plugins: 설치된 플러그인 + apperance: 모양 + website_welcome: '{{site_name}}에 오신 것을 환영합니다' + user_center: + login: 로그인 + qrcode_login_tip: '{{ agentName }}을(를) 사용하여 QR 코드를 스캔하고 로그인하세요.' + login_failed_email_tip: 로그인 실패, 다시 시도하기 전에 이 앱이 이메일 정보에 접근할 수 있도록 허용하세요. + badges: + modal: + title: 축하합니다 + content: 새로운 뱃지를 획득했습니다. + close: 닫기 + confirm: 뱃지 보기 + title: 뱃지 + awarded: 수여됨 + earned_×: '{{ number }} 개 획득' + ×_awarded: "{{ number }} 개 수여됨" + can_earn_multiple: 이 뱃지는 여러 번 획득할 수 있습니다. + earned: 획득함 + admin: + admin_header: + title: 관리자 + dashboard: + title: 대시보드 + welcome: 관리자에 오신 것을 환영합니다! + site_statistics: 사이트 통계 + questions: "질문:" + resolved: "해결됨:" + unanswered: "답변이 없는 질문:" + answers: "답변:" + comments: "댓글:" + votes: "투표:" + users: "사용자:" + flags: "신고:" + reviews: "리뷰:" + site_health: 사이트 상태 + version: "버전:" + https: "HTTPS:" + upload_folder: "업로드 폴더:" + run_mode: "실행 모드:" + private: 비공개 + public: 공개 + smtp: "SMTP:" + timezone: "시간대:" + system_info: 시스템 정보 + go_version: "Go 버전:" + database: "데이터베이스:" + database_size: "데이터베이스 크기:" + storage_used: "사용 중인 저장 공간:" + uptime: "가동 시간:" + links: 링크 + plugins: 플러그인 + github: GitHub + blog: 블로그 + contact: 연락처 + forum: 포럼 + documents: 문서 + feedback: 피드백 + support: 지원 + review: 검토 + config: 설정 + update_to: 업데이트 + latest: 최신 버전 + check_failed: 확인 실패 + "yes": "예" + "no": "아니요" + not_allowed: 허용되지 않음 + allowed: 허용됨 + enabled: 활성화됨 + disabled: 비활성화됨 + writable: 쓰기 가능 + not_writable: 쓰기 불가능 + flags: + title: 신고 + pending: 처리 대기 중 + completed: 완료됨 + flagged: 신고됨 + flagged_type: '{{ type }}로 신고됨' + created: 생성됨 + action: 동작 + review: 검토 + user_role_modal: + title: 사용자 역할 변경 + btn_cancel: 취소 + btn_submit: 제출 + new_password_modal: + title: 새 비밀번호 설정 + form: + fields: + password: + label: 비밀번호 + text: 사용자가 로그아웃되고 다시 로그인해야 합니다. + msg: 비밀번호는 8-32자여야 합니다. + btn_cancel: 취소 + btn_submit: 제출 + edit_profile_modal: + title: 프로필 수정 + form: + fields: + display_name: + label: 표시 이름 + msg_range: 표시 이름은 2-30 자 길이여야 합니다. + username: + label: 사용자 이름 + msg_range: 유저 이름은 2-30 자 길이여야 합니다. + email: + label: 이메일 + msg_invalid: 유효하지 않은 이메일 주소. + edit_success: 성공적으로 수정되었습니다 + btn_cancel: 취소 + btn_submit: 제출 + user_modal: + title: 새 사용자 추가 + form: + fields: + users: + label: 대량 사용자 추가 + placeholder: "홍길동, hong@example.com, BUSYopr2\n김철수, kim@example.com, fpDntV8q" + text: 쉼표로 구분하여 “이름, 이메일, 비밀번호”를 입력하세요. 한 줄에 한 명의 사용자. + msg: "사용자의 이메일을 입력하세요. 한 줄에 한 명씩 입력하세요." + display_name: + label: 표시 이름 + msg: 표시 이름은 2-30 자 길이여야 합니다. + email: + label: 이메일 + msg: 이메일이 유효하지 않습니다. + password: + label: 비밀번호 + msg: 비밀번호는 8-32자여야 합니다. + btn_cancel: 취소 + btn_submit: 제출 + users: + title: 사용자 + name: 이름 + email: 이메일 + reputation: 평판 + created_at: 생성 시간 + delete_at: 삭제된 시간 + suspend_at: 정지된 시간 + suspend_until: Suspend until + status: 상태 + role: 역할 + action: 동작 + change: 변경 + all: 전체 + staff: 스탭 + more: 더 보기 + inactive: 비활성화됨 + suspended: 정지됨 + deleted: 삭제됨 + normal: 일반 + Moderator: 관리자 + Admin: 관리자 + User: 사용자 + filter: + placeholder: "이름 또는 사용자 ID로 필터링" + set_new_password: 새 비밀번호 설정 + edit_profile: 프로필 수정 + change_status: 상태 변경 + change_role: 역할 변경 + show_logs: 로그 표시 + add_user: 사용자 추가 + deactivate_user: + title: 사용자 비활성화 + content: 비활성화된 사용자는 이메일을 다시 확인해야 합니다. + delete_user: + title: 이 사용자 삭제 + content: 정말로 이 사용자를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다! + remove: 사용자의 모든 질문, 답변, 댓글 등을 삭제합니다. + label: 사용자의 계정만 삭제하려면 이 옵션을 선택하지 마세요. + text: 사용자의 계정만 삭제하려면 이 항목을 선택하지 마십시오. + suspend_user: + title: 이 사용자 정지 + content: 정지된 사용자는 로그인할 수 없습니다. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: 질문 + unlisted: 비공개 + post: 게시물 + votes: 투표 + answers: 답변 + created: 생성됨 + status: 상태 + action: 동작 + change: 변경 + pending: 대기 중 + filter: + placeholder: "제목 또는 질문 ID로 필터링" + answers: + page_title: 답변 + post: 게시물 + votes: 투표 + created: 생성됨 + status: 상태 + action: 동작 + change: 변경 + filter: + placeholder: "제목 또는 답변 ID로 필터링" + general: + page_title: 일반 + name: + label: 사이트 이름 + msg: 사이트 이름을 입력하세요. + text: "사이트 이름, 타이틀 태그에 사용됩니다." + site_url: + label: 사이트 URL + msg: 사이트 URL을 입력하세요. + validate: 유효한 URL을 입력하세요. + text: 사이트 주소입니다. + short_desc: + label: 짧은 사이트 설명 + msg: 짧은 사이트 설명을 입력하세요. + text: "홈페이지에서 사용되는 짧은 설명입니다." + desc: + label: 사이트 설명 + msg: 사이트 설명을 입력하세요. + text: "메타 설명 태그에 사용되는 한 문장 설명입니다." + contact_email: + label: 연락처 이메일 + msg: 연락처 이메일을 입력하세요. + validate: 유효한 이메일 주소를 입력하세요. + text: 사이트를 책임지는 주요 연락처 이메일 주소입니다. + check_update: + label: 소프트웨어 업데이트 + text: 소프트웨어 업데이트 자동 확인 + interface: + page_title: 인터페이스 + language: + label: 인터페이스 언어 + msg: 인터페이스 언어를 선택하세요. + text: 페이지를 새로고침하면 언어가 변경됩니다. + time_zone: + label: 시간대 + msg: 시간대를 선택하세요. + text: 본인과 같은 시간대의 도시를 선택하세요. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: 발신 이메일 + msg: 발신 이메일을 입력하세요. + text: 이메일 발신 주소입니다. + from_name: + label: 발신자 이름 + msg: 발신자 이름을 입력하세요. + text: 이메일 발신 시 사용될 이름입니다. + smtp_host: + label: SMTP 호스트 + msg: SMTP 호스트를 입력하세요. + text: 메일 서버 주소입니다. + encryption: + label: 암호화 + msg: 암호화 방식을 선택하세요. + text: 대부분의 서버에서 SSL을 권장합니다. + ssl: SSL + tls: TLS + none: 없음 + smtp_port: + label: SMTP 포트 + msg: SMTP 포트는 1에서 65535 사이의 숫자여야 합니다. + text: 메일 서버의 포트 번호입니다. + smtp_username: + label: SMTP 사용자 이름 + msg: SMTP 사용자 이름을 입력하세요. + smtp_password: + label: SMTP 비밀번호 + msg: SMTP 비밀번호를 입력하세요. + test_email_recipient: + label: 테스트 이메일 수신자 + text: 테스트 이메일을 받을 이메일 주소를 입력하세요. + msg: 테스트 이메일 수신자가 유효하지 않습니다. + smtp_authentication: + label: 인증 사용 + title: SMTP 인증 + msg: SMTP 인증을 선택하세요. + "yes": "예" + "no": "아니오" + branding: + page_title: 브랜딩 + logo: + label: 로고 + msg: 로고를 입력하세요. + text: 사이트 좌측 상단에 표시될 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 하며 가로 세로 비율은 3:1 이상이어야 합니다. 비워 둘 경우 사이트 제목 텍스트가 표시됩니다. + mobile_logo: + label: 모바일 로고 + text: 사이트의 모바일 버전에서 사용할 로고 이미지입니다. 넓은 직사각형 이미지로, 높이는 56 이상이어야 합니다. 비워 둘 경우 "로고" 설정에서 이미지가 사용됩니다. + square_icon: + label: 정사각형 아이콘 + msg: 정사각형 아이콘을 입력하세요. + text: 메타데이터 아이콘의 기본 이미지로 사용됩니다. 이상적으로는 512x512보다 큰 이미지여야 합니다. + favicon: + label: 파비콘 + text: 사이트의 파비콘 이미지입니다. CDN에서 정상적으로 작동하려면 png 형식이어야 하며, 크기는 32x32로 조정됩니다. 비워 둘 경우 "정사각형 아이콘"이 사용됩니다. + legal: + page_title: 법적 고지 + terms_of_service: + label: 서비스 이용 약관 + text: "여기에 서비스 이용 약관 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요." + privacy_policy: + label: 개인정보 보호 정책 + text: "여기에 개인정보 보호 정책 내용을 추가할 수 있습니다. 이미 다른 곳에 문서가 호스팅되어 있다면 전체 URL을 여기에 제공하세요." + external_content_display: + label: 외부 콘텐츠 + text: "콘텐츠에는 외부 웹사이트에서 삽입된 이미지, 비디오 및 미디어가 포함됩니다." + always_display: 항상 외부 콘텐츠 표시 + ask_before_display: 외부 콘텐츠 표시 전 확인 + write: + page_title: 작성 + restrict_answer: + title: 답변 작성 + label: 각 사용자는 각 질문에 대해 단 하나의 답변만 작성할 수 있습니다. + text: "기존 답변을 개선하고 향상시키기 위해 편집 링크를 사용할 수 있습니다." + recommend_tags: + label: 추천 태그 + text: "추천 태그가 기본적으로 드롭다운 목록에 표시됩니다." + msg: + contain_reserved: "추천 태그에는 예약된 태그가 포함될 수 없습니다" + required_tag: + title: 필수 태그 설정 + label: '"추천 태그" 를 필수 태그로 설정' + text: "모든 새로운 질문은 최소한 하나의 추천 태그가 있어야 합니다." + reserved_tags: + label: 예약된 태그 + text: "예약된 태그는 관리자만 사용할 수 있습니다." + image_size: + label: 최대 이미지 크기 (MB) + text: "최대 이미지 업로드 크기입니다." + attachment_size: + label: 최대 첨부 파일 크기 (MB) + text: "최대 첨부 파일 업로드 크기입니다." + image_megapixels: + label: 최대 이미지 메가픽셀 + text: "이미지에 허용되는 최대 메가픽셀 수입니다." + image_extensions: + label: 허용된 이미지 확장자 + text: "이미지 표시가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요." + attachment_extensions: + label: 인증된 첨부 파일 확장자 + text: "업로드가 허용된 파일 확장자 목록입니다. 쉼표로 구분하세요. 경고: 업로드를 허용하면 보안 문제가 발생할 수 있습니다." + seo: + page_title: 검색 엔진 최적화 + permalink: + label: 영구 링크 + text: 사용자 정의 URL 구조는 링크의 사용성과 미래 호환성을 향상시킬 수 있습니다. + robots: + label: robots.txt + text: 이 설정은 사이트 설정과 관련된 내용을 영구적으로 덮어씁니다. + themes: + page_title: 테마 + themes: + label: 테마 + text: 기존 테마를 선택하세요. + color_scheme: + label: 색상 스키마 + navbar_style: + label: 네비바 배경 스타일 + primary_color: + label: 주요 색상 + text: 테마에서 사용할 색상을 수정합니다. + css_and_html: + page_title: CSS 및 HTML + custom_css: + label: 사용자 정의 CSS + text: > + + head: + label: 헤드 + text: > + + header: + label: 헤더 + text: > + + footer: + label: 푸터 + text: 본문의 바로 앞에 삽입됩니다. + sidebar: + label: 사이드바 + text: 사이드바에 삽입됩니다. + login: + page_title: 로그인 + membership: + title: 멤버십 + label: 신규 등록 허용 + text: 계정을 생성할 수 있는 사람을 제한하려면 끄세요. + email_registration: + title: 이메일 등록 + label: 이메일 등록 허용 + text: 이메일을 통한 새 계정 생성을 막으려면 끄세요. + allowed_email_domains: + title: 허용된 이메일 도메인 + text: 사용자가 계정을 등록할 때 필수적으로 사용해야 하는 이메일 도메인입니다. 한 줄에 하나의 도메인을 입력하세요. 비어 있으면 무시됩니다. + private: + title: 비공개 + label: 로그인 필수 + text: 로그인한 사용자만이 이 커뮤니티에 접근할 수 있습니다. + password_login: + title: 비밀번호 로그인 + label: 이메일과 비밀번호 로그인 허용 + text: "경고: 끄면 다른 로그인 방법을 설정하지 않았다면 로그인할 수 없을 수 있습니다." + installed_plugins: + title: 설치된 플러그인 + plugin_link: 플러그인은 기능을 확장하고 확장합니다. <1> 플러그인 저장소에서 플러그인을 찾을 수 있습니다. + filter: + all: 전체 + active: 활성화됨 + inactive: 비활성화됨 + outdated: 오래된 상태 + plugins: + label: 플러그인 + text: 기존 플러그인을 선택하세요. + name: 이름 + version: 버전 + status: 상태 + action: 작업 + deactivate: 비활성화 + activate: 활성화 + settings: 설정 + settings_users: + title: 사용자 + avatar: + label: 기본 아바타 + text: 사용자가 자신의 사용자 정의 아바타를 가지지 않았을 때 표시됩니다. + gravatar_base_url: + label: Gravatar 기본 URL + text: Gravatar 공급자의 API 기본 URL입니다. 비어 있으면 무시됩니다. + profile_editable: + title: 프로필 편집 가능 + allow_update_display_name: + label: 사용자가 표시 이름을 변경할 수 있도록 허용 + allow_update_username: + label: 사용자가 사용자 이름을 변경할 수 있도록 허용 + allow_update_avatar: + label: 사용자가 프로필 이미지를 변경할 수 있도록 허용 + allow_update_bio: + label: 사용자가 자기 소개를 변경할 수 있도록 허용 + allow_update_website: + label: 사용자가 웹사이트를 변경할 수 있도록 허용 + allow_update_location: + label: 사용자가 위치 정보를 변경할 수 있도록 허용 + privilege: + title: 권한 + level: + label: 권한에 필요한 평판 레벨 + text: 권한에 필요한 평판 레벨을 선택하세요. + msg: + should_be_number: 입력값은 숫자여야 합니다. + number_larger_1: 숫자는 1 이상이어야 합니다. + badges: + action: 동작 + active: 활성 + activate: 활성화 + all: 모두 + awards: 수상 + deactivate: 비활성화 + filter: + placeholder: 이름, 배지:id 로 필터링 + group: 그룹 + inactive: 비활성 + name: 이름 + show_logs: 로그 표시 + status: 상태 + title: 뱃지 + form: + optional: (선택 사항) + empty: 비어 있을 수 없습니다 + invalid: 유효하지 않습니다 + btn_submit: 저장 + not_found_props: "필수 속성 {{ key }}을(를) 찾을 수 없습니다." + select: 선택 + page_review: + review: 리뷰 + proposed: 제안된 + question_edit: 질문 편집 + answer_edit: 답변 편집 + tag_edit: 태그 편집 + edit_summary: 편집 요약 + edit_question: 질문 편집 + edit_answer: 답변 편집 + edit_tag: 태그 편집 + empty: 남은 리뷰 작업이 없습니다. + approve_revision_tip: 이 리비전을 승인하시겠습니까? + approve_flag_tip: 이 신고를 승인하시겠습니까? + approve_post_tip: 이 게시물을 승인하시겠습니까? + approve_user_tip: 이 사용자를 승인하시겠습니까? + suggest_edits: 제안된 편집 + flag_post: 게시물 신고 + flag_user: 사용자 신고 + queued_post: 대기 중인 게시물 + queued_user: 대기 중인 사용자 + filter_label: 유형 + reputation: 평판 + flag_post_type: 이 게시물을 {{ type }}로 신고 처리했습니다. + flag_user_type: 이 사용자를 {{ type }}로 신고 처리했습니다. + edit_post: 게시물 편집 + list_post: 게시물 목록 + unlist_post: 게시물 비공개 + timeline: + undeleted: 복구됨 + deleted: 삭제됨 + downvote: 다운보트 + upvote: 업보트 + accept: 채택됨 + cancelled: 취소됨 + commented: 댓글 작성됨 + rollback: 롤백 + edited: 편집됨 + answered: 답변됨 + asked: 질문됨 + closed: 닫힘 + reopened: 다시 열림 + created: 생성됨 + pin: 고정됨 + unpin: 고정 해제됨 + show: 공개됨 + hide: 비공개됨 + title: "다음을 위한 히스토리" + tag_title: "태그에 대한 타임라인" + show_votes: "투표 보기" + n_or_a: 없음 + title_for_question: "질문에 대한 타임라인" + title_for_answer: "{{ author }}가 {{ title }}에 대한 답변에 대한 타임라인" + title_for_tag: "태그에 대한 타임라인" + datetime: 날짜 및 시간 + type: 유형 + by: 작성자 + comment: 코멘트 + no_data: "아무 데이터도 찾을 수 없습니다." + users: + title: 사용자 + users_with_the_most_reputation: 이번 주 평판이 가장 높은 사용자들 + users_with_the_most_vote: 이번 주 투표를 가장 많이 한 사용자들 + staffs: 우리 커뮤니티 스태프 + reputation: 평판 + votes: 투표 + prompt: + leave_page: 페이지를 떠나시겠습니까? + changes_not_save: 변경 사항이 저장되지 않을 수 있습니다. + draft: + discard_confirm: 초안을 삭제하시겠습니까? + messages: + post_deleted: 이 게시물은 삭제되었습니다. + post_cancel_deleted: 이 게시물이 삭제 취소되었습니다. + post_pin: 이 게시물이 고정되었습니다. + post_unpin: 이 게시물의 고정이 해제되었습니다. + post_hide_list: 이 게시물이 목록에서 숨겨졌습니다. + post_show_list: 이 게시물이 목록에 표시되었습니다. + post_reopen: 이 게시물이 다시 열렸습니다. + post_list: 이 게시물이 목록에 등록되었습니다. + post_unlist: 이 게시물이 목록에서 등록 해제되었습니다. + post_pending: 회원님의 게시물이 검토를 기다리고 있습니다. 미리보기입니다. 승인 후에 공개됩니다. + post_closed: 이 게시물이 닫혔습니다. + answer_deleted: 이 답변이 삭제되었습니다. + answer_cancel_deleted: 이 답변이 삭제 취소되었습니다. + change_user_role: 이 사용자의 역할이 변경되었습니다. + user_inactive: 이 사용자는 이미 비활성 상태입니다. + user_normal: 이 사용자는 이미 일반 사용자입니다. + user_suspended: 이 사용자가 정지되었습니다. + user_deleted: 이 사용자가 삭제되었습니다. + badge_activated: 이 배지가 활성화되었습니다. + badge_inactivated: 이 배지가 비활성화되었습니다. + users_deleted: 이 사용자들이 삭제되었습니다. + posts_deleted: 이 질문들이 삭제되었습니다. + answers_deleted: 이 답변들이 삭제되었습니다. + copy: 클립보드에 복사 + copied: 복사됨 + external_content_warning: 외부 이미지/미디어가 표시되지 않습니다. + + diff --git a/i18n/ml_IN.yaml b/i18n/ml_IN.yaml new file mode 100644 index 000000000..c42260585 --- /dev/null +++ b/i18n/ml_IN.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: List + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: Email + e_mail: + other: Email + password: + other: Password + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Email and password do not match. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n

{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Edit Tag + ask_a_question: Create Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + new_alerts: New alerts + all_read: Mark all as read + show_more: Show more + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Invite People + desc: Invite people you think can answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for the same question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/nl_NL.yaml b/i18n/nl_NL.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/nl_NL.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml new file mode 100644 index 000000000..a0cd01396 --- /dev/null +++ b/i18n/no_NO.yaml @@ -0,0 +1,1385 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/pl_PL.yaml b/i18n/pl_PL.yaml new file mode 100644 index 000000000..21562f911 --- /dev/null +++ b/i18n/pl_PL.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Sukces. + unknown: + other: Nieznany błąd. + request_format_error: + other: Format żądania jest nieprawidłowy. + unauthorized_error: + other: Niezautoryzowany. + database_error: + other: Błąd serwera danych. + forbidden_error: + other: Zakazane. + duplicate_request_error: + other: Duplikat zgłoszenia. + action: + report: + other: Zgłoś + edit: + other: Edytuj + delete: + other: Usuń + close: + other: Zamknij + reopen: + other: Otwórz ponownie + forbidden_error: + other: Zakazane. + pin: + other: Przypnij + hide: + other: Usuń z listy + unpin: + other: Odepnij + show: + other: Lista + invite_someone_to_answer: + other: Edytuj + undelete: + other: Przywróć + merge: + other: Merge + role: + name: + user: + other: Użytkownik + admin: + other: Administrator + moderator: + other: Moderator + description: + user: + other: Domyślnie bez specjalnego dostępu. + admin: + other: Posiadać pełne uprawnienia dostępu do strony. + moderator: + other: Ma dostęp do wszystkich postów z wyjątkiem ustawień administratora. + privilege: + level_1: + description: + other: Poziom 1 (mniejsza reputacja wymagana dla prywatnego zespołu, grupy) + level_2: + description: + other: Poziom 2 (niska reputacja wymagana dla społeczności startującej) + level_3: + description: + other: Poziom 3 (wysoka reputacja wymagana dla dojrzałej społeczności) + level_custom: + description: + other: Poziom niestandardowy + rank_question_add_label: + other: Zadaj pytanie + rank_answer_add_label: + other: Napisz odpowiedź + rank_comment_add_label: + other: Napisz komentarz + rank_report_add_label: + other: Zgłoś + rank_comment_vote_up_label: + other: Wyróżnij komentarz + rank_link_url_limit_label: + other: Opublikuj więcej niż 2 linki na raz + rank_question_vote_up_label: + other: Wyróżnij pytanie + rank_answer_vote_up_label: + other: Wyróżnij odpowiedź + rank_question_vote_down_label: + other: Oceń pytanie negatywnie + rank_answer_vote_down_label: + other: Oceń odpowiedź negatywnie + rank_invite_someone_to_answer_label: + other: Zaproś kogoś do odpowiedzi + rank_tag_add_label: + other: Utwórz nowy tag + rank_tag_edit_label: + other: Edytuj opis tagu (wymaga akceptacji) + rank_question_edit_label: + other: Edytuj pytanie innych (wymaga akceptacji) + rank_answer_edit_label: + other: Edytuj odpowiedź innych (wymaga akceptacji) + rank_question_edit_without_review_label: + other: Edytuj pytanie innych bez akceptacji + rank_answer_edit_without_review_label: + other: Edytuj odpowiedź innych bez akceptacji + rank_question_audit_label: + other: Przejrzyj edycje pytania + rank_answer_audit_label: + other: Przejrzyj edycje odpowiedzi + rank_tag_audit_label: + other: Przejrzyj edycje tagu + rank_tag_edit_without_review_label: + other: Edytuj opis tagu bez akceptacji + rank_tag_synonym_label: + other: Zarządzaj synonimami tagów + email: + other: E-mail + e_mail: + other: Email + password: + other: Hasło + pass: + other: Hasło + old_pass: + other: Current password + original_text: + other: Ten wpis + email_or_password_wrong_error: + other: Email lub hasło nie są poprawne. + error: + common: + invalid_url: + other: Nieprawidłowy URL. + status_invalid: + other: Nieprawidłowy status. + password: + space_invalid: + other: Hasło nie może zawierać spacji. + admin: + cannot_update_their_password: + other: Nie możesz zmieniać swojego hasła. + cannot_edit_their_profile: + other: Nie możesz modyfikować swojego profilu. + cannot_modify_self_status: + other: Nie możesz modyfikować swojego statusu. + email_or_password_wrong: + other: Emil lub hasło nie są zgodne. + answer: + not_found: + other: Odpowiedź nie została odnaleziona. + cannot_deleted: + other: Brak uprawnień do usunięcia. + cannot_update: + other: Brak uprawnień do aktualizacji. + question_closed_cannot_add: + other: Pytania są zamknięte i nie można ich dodawać. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Komentarz nie może edytować. + not_found: + other: Komentarz nie został odnaleziony. + cannot_edit_after_deadline: + other: Czas komentowania był zbyt długi, aby go zmodyfikować. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: E-mail już istnieje. + need_to_be_verified: + other: E-mail powinien zostać zweryfikowany. + verify_url_expired: + other: Adres URL zweryfikowanej wiadomości e-mail wygasł, prosimy o ponowne wysłanie wiadomości e-mail. + illegal_email_domain_error: + other: Wysyłanie wiadomości e-mail z tej domeny jest niedozwolone. Użyj innej domeny. + lang: + not_found: + other: Nie znaleziono pliku językowego. + object: + captcha_verification_failed: + other: Nieprawidłowa captcha. + disallow_follow: + other: Nie wolno ci podążać za nimi. + disallow_vote: + other: Nie masz uprawnień do głosowania. + disallow_vote_your_self: + other: Nie możesz głosować na własne posty. + not_found: + other: Obiekt nie został odnaleziony. + verification_failed: + other: Weryfikacja nie powiodła się. + email_or_password_incorrect: + other: Email lub hasło są nieprawidłowe. + old_password_verification_failed: + other: Stara weryfikacja hasła nie powiodła się + new_password_same_as_previous_setting: + other: Nowe hasło jest takie samo jak poprzednie. + already_deleted: + other: Ten wpis został usunięty. + meta: + object_not_found: + other: Meta obiekt nie został odnaleziony + question: + already_deleted: + other: Ten post został usunięty. + under_review: + other: Twój post oczekuje na recenzje. Będzie widoczny po jej akceptacji. + not_found: + other: Pytanie nie zostało odnalezione. + cannot_deleted: + other: Brak uprawnień do usunięcia. + cannot_close: + other: Brak uprawnień do zamknięcia. + cannot_update: + other: Brak uprawnień do edycji. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Ranga nie spełnia warunku. + vote_fail_to_meet_the_condition: + other: Dziękujemy za opinię. Potrzebujesz co najmniej {{.Rank}} reputacji, aby oddać głos. + no_enough_rank_to_operate: + other: Potrzebujesz co najmniej {{.Rank}} reputacji, aby to zrobić. + report: + handle_failed: + other: Nie udało się obsłużyć raportu. + not_found: + other: Raport nie został znaleziony. + tag: + already_exist: + other: Tag już istnieje. + not_found: + other: Tag nie został znaleziony. + recommend_tag_not_found: + other: Zalecany tag nie istnieje. + recommend_tag_enter: + other: Proszę wprowadzić przynajmniej jeden wymagany tag. + not_contain_synonym_tags: + other: Nie powinno zawierać tagów synonimów. + cannot_update: + other: Brak uprawnień do aktualizacji. + is_used_cannot_delete: + other: Nie możesz usunąć tagu, który jest w użyciu. + cannot_set_synonym_as_itself: + other: Nie można ustawić synonimu aktualnego tagu jako takiego. + smtp: + config_from_name_cannot_be_email: + other: Nazwą nadawcy nie może być adresem e-mail. + theme: + not_found: + other: Nie znaleziono motywu. + revision: + review_underway: + other: Nie można teraz edytować, istnieje wersja w kolejce sprawdzeń. + no_permission: + other: Brak uprawnień do wersji. + user: + external_login_missing_user_id: + other: Platforma zewnętrzna nie dostarcza unikalnego identyfikatora użytkownika (UserID), dlatego nie możesz się zalogować. Skontaktuj się z administratorem witryny. + external_login_unbinding_forbidden: + other: Proszę ustawić hasło logowania dla swojego konta przed usunięciem tego logowania. + email_or_password_wrong: + other: + other: Adres email i hasło nie są zgodne. + not_found: + other: Użytkownik nie został znaleziony. + suspended: + other: Użytkownik został zawieszony. + username_invalid: + other: Nazwa użytkownika jest nieprawidłowa. + username_duplicate: + other: Nazwa użytkownika jest już zajęta. + set_avatar: + other: Nie udało się ustawić awatara. + cannot_update_your_role: + other: Nie możesz zmienić swojej roli. + not_allowed_registration: + other: Obecnie strona nie zezwala na rejestracje. + not_allowed_login_via_password: + other: Obecnie strona nie zezwala na logowanie się za pomocą hasła. + access_denied: + other: Odmowa dostępu. + page_access_denied: + other: Nie masz dostępu do tej strony. + add_bulk_users_format_error: + other: "Błąd {{.Field}} w pobliżu '{{.Content}}' w linii {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Liczba użytkowników, których dodasz na raz, powinna mieścić się w przedziale 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Nie udało się odczytać pliku konfiguracyjnego. + database: + connection_failed: + other: Nie udało się połączyć z bazą danych. + create_table_failed: + other: Nie udało się utworzyć tabeli. + install: + create_config_failed: + other: Nie można utworzyć pliku config.yaml. + upload: + unsupported_file_format: + other: Nieobsługiwany format pliku. + site_info: + config_not_found: + other: Nie znaleziono konfiguracji strony. + badge: + object_not_found: + other: Nie znaleziono obiektu odznaki + reason: + spam: + name: + other: spam + desc: + other: Ten post jest reklamą lub wandalizmem. Nie jest przydatny ani istotny dla bieżącego tematu. + rude_or_abusive: + name: + other: niegrzeczny lub obraźliwy + desc: + other: "Rozsądna osoba uznałaby tę treść za nieodpowiednią do dyskusji opartej na szacunku." + a_duplicate: + name: + other: duplikat + desc: + other: To pytanie zostało już wcześniej zadane i ma już odpowiedź. + placeholder: + other: Wprowadź link do istniejącego pytania + not_a_answer: + name: + other: nie jest odpowiedzią + desc: + other: "Ta wiadomość została zamieszczona jako odpowiedź, ale nie próbuje odpowiedzieć na pytanie. Powinna być prawdopodobnie edycją, komentarzem, kolejnym pytaniem lub całkowicie usunięta." + no_longer_needed: + name: + other: nie jest już potrzebne + desc: + other: Ten komentarz jest przestarzały, prowadzi do rozmowy lub nie jest związany z tą wiadomością. + something: + name: + other: coś innego + desc: + other: Ta wiadomość wymaga uwagi personelu z innego powodu, który nie jest wymieniony powyżej. + placeholder: + other: Poinformuj nas dokładnie, o co Ci chodzi + community_specific: + name: + other: powód specyficzny dla społeczności + desc: + other: To pytanie nie spełnia wytycznych społeczności. + not_clarity: + name: + other: wymaga szczegółów lub wyjaśnienia + desc: + other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno skupić się tylko na jednym problemie. + looks_ok: + name: + other: Wygląda poprawnie + desc: + other: Ta wiadomość jest dobra w obecnej formie i nie jest niskiej jakości. + needs_edit: + name: + other: wymaga edycji, a ja to zrobiłem/am + desc: + other: Popraw i skoryguj problemy w tej wiadomości samodzielnie. + needs_close: + name: + other: wymaga zamknięcia + desc: + other: Na zamknięte pytanie nie można odpowiadać, ale wciąż można edytować, głosować i komentować. + needs_delete: + name: + other: wymaga usunięcia + desc: + other: Ta wiadomość zostanie usunięta. + question: + close: + duplicate: + name: + other: spam + desc: + other: To pytanie zostało już wcześniej zadane i ma już odpowiedź. + guideline: + name: + other: powód specyficzny dla społeczności + desc: + other: To pytanie nie spełnia wytycznych społeczności. + multiple: + name: + other: wymaga szczegółów lub wyjaśnienia + desc: + other: To pytanie obecnie zawiera wiele pytań w jednym. Powinno się skupić tylko na jednym problemie. + other: + name: + other: coś innego + desc: + other: Ten post wymaga jeszcze jednego powodu, który nie został wymieniony powyżej. + operation_type: + asked: + other: zapytano + answered: + other: odpowiedziano + modified: + other: zmodyfikowane + deleted_title: + other: Usunięte pytanie + questions_title: + other: Pytania + tag: + tags_title: + other: Tagi + no_description: + other: Tag nie posiada opisu. + notification: + action: + update_question: + other: zaktualizował/a pytanie + answer_the_question: + other: odpowiedz na pytanie + update_answer: + other: aktualizuj odpowiedź + accept_answer: + other: zaakceptował/a odpowiedź + comment_question: + other: skomentował/a pytanie + comment_answer: + other: skomentował/a odpowiedź + reply_to_you: + other: odpowiedział/a tobie + mention_you: + other: wspomniał/a o tobie + your_question_is_closed: + other: Twoje pytanie zostało zamknięte + your_question_was_deleted: + other: Twoje pytanie zostało usunięte + your_answer_was_deleted: + other: Twoja odpowiedź została usunięta + your_comment_was_deleted: + other: Twój komentarz został usunięty + up_voted_question: + other: pytanie przegłosowane + down_voted_question: + other: pytanie odrzucone + up_voted_answer: + other: odpowiedź przegłosowana + down_voted_answer: + other: odrzucona odpowiedź + up_voted_comment: + other: komentarz upvote + invited_you_to_answer: + other: zaproszono Cię do odpowiedzi + earned_badge: + other: Zdobyłeś odznakę "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Potwierdź swój nowy adres e-mail" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} odpowiedział(-a) na pytanie" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} zaprosił(a) Cię do odpowiedzi" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} skomentował/-a Twój wpis" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] Nowe pytanie: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Reset hasła" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Potwierdź swoje nowe konto" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Wiadomość testowa" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: oceń pozytywnie + upvoted: + other: polubione + downvote: + other: oceń negatywnie + downvoted: + other: oceniono negatywnie + accept: + other: akceptuj + accepted: + other: zaakceptowane + edit: + other: edytuj + review: + queued_post: + other: Post w kolejce + flagged_post: + other: Post oznaczony + suggested_post_edit: + other: Sugerowane zmiany + reaction: + tooltip: + other: "{{ .Names }} już {{ .Count }} razy ..." + badge: + default_badges: + autobiographer: + name: + other: Autobiografista + desc: + other: Wypełniono informacje profil. + certified: + name: + other: Certyfikowany + desc: + other: Ukończono nasz nowy samouczek. + editor: + name: + other: Edytor + desc: + other: Pierwsza edycja posta. + first_flag: + name: + other: Pierwsza flaga + desc: + other: Po raz pierwszy oznaczono post. + first_upvote: + name: + other: Pierwszy pozytywny głos + desc: + other: First up voted a post. + first_link: + name: + other: Pierwszy odnośnik + desc: + other: First added a link to another post. + first_reaction: + name: + other: Pierwsza Reakcja + desc: + other: First reacted to the post. + first_share: + name: + other: Pierwsze udostępnianie + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Zadane pytania i zaakceptowane odpowiedź. + commentator: + name: + other: Commentator + desc: + other: Pozostaw 5 komentarzy. + new_user_of_the_month: + name: + other: Nowy użytkownik miesiąca + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Jak formatować + desc: >- + + pagination: + prev: Poprzedni + next: Następny + page_title: + question: Pytanie + questions: Pytania + tag: Tag + tags: Tagi + tag_wiki: wiki tagu + create_tag: Utwórz tag + edit_tag: Edytuj tag + ask_a_question: Create Question + edit_question: Edytuj pytanie + edit_answer: Edytuj odpowiedź + search: Szukaj + posts_containing: Posty zawierające + settings: Ustawienia + notifications: Powiadomienia + login: Zaloguj się + sign_up: Zarejestruj się + account_recovery: Odzyskiwanie konta + account_activation: Aktywacja konta + confirm_email: Potwierdź adres e-mail + account_suspended: Konto zawieszone + admin: Administrator + change_email: Zmień e-mail + install: Instalacja Answer + upgrade: Aktualizacja Answer + maintenance: Przerwa techniczna + users: Użytkownicy + oauth_callback: Przetwarzanie + http_404: Błąd HTTP 404 + http_50X: Błąd HTTP 500 + http_403: Błąd HTTP 403 + logout: Wyloguj się + notifications: + title: Powiadomienia + inbox: Skrzynka odbiorcza + achievement: Osiągnięcia + new_alerts: Nowe powiadomienia + all_read: Oznacz wszystkie jako przeczytane + show_more: Pokaż więcej + someone: Ktoś + inbox_type: + all: Wszystko + posts: Posty + invites: Zaproszenia + votes: Głosy + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Twoje konto zostało zawieszone + until_time: "Twoje konto zostało zawieszone do {{ time }}." + forever: Ten użytkownik został na zawsze zawieszony. + end: Nie spełniasz wytycznych społeczności. + contact_us: Skontaktuj się z nami + editor: + blockquote: + text: Cytat + bold: + text: Pogrubienie + chart: + text: Wykres + flow_chart: Wykres przepływu + sequence_diagram: Diagram sekwencji + class_diagram: Diagram klas + state_diagram: Diagram stanów + entity_relationship_diagram: Diagram związków encji + user_defined_diagram: Diagram zdefiniowany przez użytkownika + gantt_chart: Wykres Gantta + pie_chart: Wykres kołowy + code: + text: Przykład kodu + add_code: Dodaj przykład kodu + form: + fields: + code: + label: Kod + msg: + empty: Kod nie może być pusty. + language: + label: Język + placeholder: Wykrywanie automatyczne + btn_cancel: Anuluj + btn_confirm: Dodaj + formula: + text: Formuła + options: + inline: Formuła w linii + block: Formuła blokowa + heading: + text: Nagłówek + options: + h1: Nagłówek 1 + h2: Nagłówek 2 + h3: Nagłówek 3 + h4: Nagłówek 4 + h5: Nagłówek 5 + h6: Nagłówek 6 + help: + text: Pomoc + hr: + text: Linia pozioma + image: + text: Obrazek + add_image: Dodaj obrazek + tab_image: Prześlij obrazek + form_image: + fields: + file: + label: Plik graficzny + btn: Wybierz obrazek + msg: + empty: Plik nie może być pusty. + only_image: Dozwolone są tylko pliki obrazków. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Opis + tab_url: Adres URL obrazka + form_url: + fields: + url: + label: Adres URL obrazka + msg: + empty: Adres URL obrazka nie może być pusty. + name: + label: Opis + btn_cancel: Anuluj + btn_confirm: Dodaj + uploading: Przesyłanie + indent: + text: Wcięcie + outdent: + text: Wcięcie zewnętrzne + italic: + text: Podkreślenie + link: + text: Hiperłącze + add_link: Dodaj hiperłącze + form: + fields: + url: + label: Adres URL + msg: + empty: Adres URL nie może być pusty. + name: + label: Opis + btn_cancel: Anuluj + btn_confirm: Dodaj + ordered_list: + text: Lista numerowana + unordered_list: + text: Lista wypunktowana + table: + text: Tabela + heading: Nagłówek + cell: Komórka + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Zamykam ten post jako... + btn_cancel: Anuluj + btn_submit: Prześlij + remark: + empty: Nie może być puste. + msg: + empty: Proszę wybrać powód. + report_modal: + flag_title: Oznaczam ten post jako... + close_title: Zamykam ten post jako... + review_question_title: Przegląd pytania + review_answer_title: Przegląd odpowiedzi + review_comment_title: Przegląd komentarza + btn_cancel: Anuluj + btn_submit: Prześlij + remark: + empty: Nie może być puste. + msg: + empty: Proszę wybrać powód. + not_a_url: Format adresu URL jest nieprawidłowy. + url_not_match: Wskazany URL nie pasuje do bieżącej witryny. + tag_modal: + title: Utwórz nowy tag + form: + fields: + display_name: + label: Nazwa wyświetlana + msg: + empty: Nazwa wyświetlana nie może być pusta. + range: Nazwa wyświetlana może zawierać maksymalnie 35 znaków. + slug_name: + label: Adres URL slug + desc: Odnośnik URL może zawierać maksymalnie 35 znaków. + msg: + empty: Odnośnik URL nie może być pusty. + range: Odnośnik URL może zawierać maksymalnie 35 znaków. + character: Slug adresu URL zawiera niedozwolony zestaw znaków. + desc: + label: Opis + revision: + label: Wersja + edit_summary: + label: Podsumowanie edycji + placeholder: >- + Krótkie wyjaśnienie zmian (poprawa pisowni, naprawa gramatyki, poprawa formatowania) + btn_cancel: Anuluj + btn_submit: Prześlij + btn_post: Opublikuj nowy tag + tag_info: + created_at: Utworzony + edited_at: Edytowany + history: Historia + synonyms: + title: Synonimy + text: Następujące tagi zostaną przekierowane na + empty: Nie znaleziono synonimów. + btn_add: Dodaj synonim + btn_edit: Edytuj + btn_save: Zapisz + synonyms_text: Następujące tagi zostaną przekierowane na + delete: + title: Usuń ten tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Czy na pewno chcesz usunąć? + close: Zamknij + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edytuj tag + default_reason: Edytuj tag + default_first_reason: Dodaj tag + btn_save_edits: Zapisz edycje + btn_cancel: Anuluj + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [o] HH:mm" + now: teraz + x_seconds_ago: "{{count}} s temu" + x_minutes_ago: "{{count}} min temu" + x_hours_ago: "{{count}} h temu" + hour: godzina + day: dzień + hours: godziny + days: dni + month: month + months: months + year: year + reaction: + heart: serce + smile: uśmiech + frown: niezadowolenie + btn_label: dodaj lub usuń reakcje + undo_emoji: cofnij reakcje {{ emoji }} + react_emoji: zareaguj {{ emoji }} + unreact_emoji: usuń reakcje {{ emoji }} + comment: + btn_add_comment: Dodaj komentarz + reply_to: Odpowiedź na + btn_reply: Odpowiedz + btn_edit: Edytuj + btn_delete: Usuń + btn_flag: Zgłoś + btn_save_edits: Zapisz edycje + btn_cancel: Anuluj + show_more: "Jest {{count}} komentarzy, pokaż więcej" + tip_question: >- + Użyj komentarzy, aby poprosić o dodatkowe informacje lub sugerować poprawki. Unikaj udzielania odpowiedzi na pytania w komentarzach. + tip_answer: >- + Użyj komentarzy, aby odpowiedzieć innym użytkownikom lub powiadomić ich o zmianach. Jeśli dodajesz nowe informacje, edytuj swój post zamiast komentować. + tip_vote: Dodaje coś wartościowego do posta + edit_answer: + title: Edytuj odpowiedź + default_reason: Edytuj odpowiedź + default_first_reason: Dodaj odpowiedź + form: + fields: + revision: + label: Rewizja + answer: + label: Odpowiedź + feedback: + characters: Treść musi mieć co najmniej 6 znaków. + edit_summary: + label: Podsumowanie edycji + placeholder: >- + Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania) + btn_save_edits: Zapisz edycje + btn_cancel: Anuluj + tags: + title: Tagi + sort_buttons: + popular: Popularne + name: Nazwa + newest: Najnowsze + button_follow: Obserwuj + button_following: Obserwowane + tag_label: pytania + search_placeholder: Filtruj według nazwy tagu + no_desc: Tag nie posiada opisu. + more: Więcej + wiki: Wiki + ask: + title: Create Question + edit_title: Edytuj pytanie + default_reason: Edytuj pytanie + default_first_reason: Create question + similar_questions: Podobne pytania + form: + fields: + revision: + label: Rewizja + title: + label: Tytuł + placeholder: What's your topic? Be specific. + msg: + empty: Tytuł nie może być pusty. + range: Tytuł do 150 znaków + body: + label: Treść + msg: + empty: Treść nie może być pusta. + tags: + label: Tagi + msg: + empty: Tagi nie mogą być puste. + answer: + label: Odpowiedź + msg: + empty: Odpowiedź nie może być pusta. + edit_summary: + label: Podsumowanie edycji + placeholder: >- + Pokrótce opisz swoje zmiany (poprawa pisowni, naprawa gramatyki, poprawa formatowania) + btn_post_question: Opublikuj swoje pytanie + btn_save_edits: Zapisz edycje + answer_question: Odpowiedz na swoje pytanie + post_question&answer: Opublikuj swoje pytanie i odpowiedź + tag_selector: + add_btn: Dodaj tag + create_btn: Utwórz nowy tag + search_tag: Wyszukaj tag + hint: "Describe what your content is about, at least one tag is required." + no_result: Nie znaleziono pasujących tagów + tag_required_text: Wymagany tag (co najmniej jeden) + header: + nav: + question: Pytania + tag: Tagi + user: Użytkownicy + badges: Badges + profile: Profil + setting: Ustawienia + logout: Wyloguj + admin: Administrator + review: Recenzja + bookmark: Zakładki + moderation: Moderacja + search: + placeholder: Szukaj + footer: + build_on: >- + Zbudowane na platformie <1> Apache Answer - oprogramowanie open-source, które napędza społeczności pytań i odpowiedzi.
Stworzone z miłością © {{cc}}. + upload_img: + name: Zmień + loading: Wczytywanie... + pic_auth_code: + title: Captcha + placeholder: Wpisz tekst z obrazka powyżej + msg: + empty: Captcha nie może być pusty. + inactive: + first: >- + Czas na ostatni krok! Wysłaliśmy wiadomość aktywacyjną na adres {{mail}}. Prosimy postępować zgodnie z instrukcjami zawartymi w wiadomości w celu aktywacji Twojego konta. + info: "Jeśli nie dotarła, sprawdź folder ze spamem." + another: >- + Wysłaliśmy kolejną wiadomość aktywacyjną na adres {{mail}}. Może to potrwać kilka minut, zanim dotrze; upewnij się, że sprawdzasz folder ze spamem. + btn_name: Ponownie wyślij wiadomość aktywacyjną + change_btn_name: Zmień adres e-mail + msg: + empty: Nie może być puste. + resend_email: + url_label: Czy na pewno chcesz ponownie wysłać e-mail aktywacyjny? + url_text: Możesz również podać powyższy link aktywacyjny użytkownikowi. + login: + login_to_continue: Zaloguj się, aby kontynuować + info_sign: Nie masz jeszcze konta? <1>Zarejestruj się + info_login: Masz już konto? <1>Zaloguj się + agreements: Rejestrując się, wyrażasz zgodę na <1>politykę prywatności i <3>warunki korzystania z usługi. + forgot_pass: Zapomniałeś hasła? + name: + label: Imię + msg: + empty: Imię nie może być puste. + range: Name must be between 2 to 30 characters in length. + character: 'Możesz użyć dozwolone znaki "a-z", "A-Z", "0-9", " - . _"' + email: + label: Adres e-mail + msg: + empty: Adres e-mail nie może być pusty. + password: + label: Hasło + msg: + empty: Hasło nie może być puste. + different: Wprowadzone hasła są niezgodne + account_forgot: + page_title: Zapomniałeś hasła + btn_name: Wyślij mi e-mail odzyskiwania + send_success: >- + Jeśli istnieje konto powiązane z adresem {{mail}}, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi resetowania hasła. + email: + label: Adres e-mail + msg: + empty: Adres e-mail nie może być pusty. + change_email: + btn_cancel: Anuluj + btn_update: Zaktualizuj adres e-mail + send_success: >- + Jeśli istnieje konto powiązane z adresem {{mail}}, wkrótce otrzymasz wiadomość e-mail z instrukcjami dotyczącymi zmiany adresu e-mail. + email: + label: Nowy email + msg: + empty: Adres e-mail nie może być pusty. + oauth: + connect: Połącz z {{ auth_name }} + remove: Usuń {{ auth_name }} + oauth_bind_email: + subtitle: Dodaj e-mail odzyskiwania do swojego konta. + btn_update: Zaktualizuj adres e-mail + email: + label: Adres e-mail + msg: + empty: Adres e-mail nie może być pusty. + modal_title: Adres e-mail już istnieje. + modal_content: Ten adres e-mail jest już zarejestrowany. Czy na pewno chcesz połączyć się z istniejącym kontem? + modal_cancel: Zmień adres e-mail + modal_confirm: Połącz z istniejącym kontem + password_reset: + page_title: Resetowanie hasła + btn_name: Zresetuj moje hasło + reset_success: >- + Pomyślnie zmieniono hasło; zostaniesz przekierowany na stronę logowania. + link_invalid: >- + Przepraszamy, ten link do resetowania hasła jest już nieaktualny. Być może Twoje hasło jest już zresetowane? + to_login: Przejdź do strony logowania + password: + label: Hasło + msg: + empty: Hasło nie może być puste. + length: Długość musi wynosić od 8 do 32 znaków. + different: Wprowadzone hasła są niezgodne. + password_confirm: + label: Potwierdź nowe hasło + settings: + page_title: Ustawienia + goto_modify: Przejdź do modyfikacji + nav: + profile: Profil + notification: Powiadomienia + account: Konto + interface: Interfejs + profile: + heading: Profil + btn_name: Zapisz + display_name: + label: Nazwa wyświetlana + msg: Wyświetlana nazwa nie może być pusta. + msg_range: Display name must be 2-30 characters in length. + username: + label: Nazwa użytkownika + caption: Ludzie mogą oznaczać Cię jako "@nazwa_użytkownika". + msg: Nazwa użytkownika nie może być pusta. + msg_range: Username must be 2-30 characters in length. + character: 'Należy używać zestawu znaków "a-z", "0-9", " - . _"' + avatar: + label: Zdjęcie profilowe + gravatar: Gravatar + gravatar_text: Możesz zmienić obraz na stronie + custom: Własne + custom_text: Możesz przesłać własne zdjęcie. + default: Systemowe + msg: Prosimy o przesłanie awatara + bio: + label: O mnie + website: + label: Strona internetowa + placeholder: "https://przyklad.com" + msg: Nieprawidłowy format strony internetowej + location: + label: Lokalizacja + placeholder: "Miasto, Kraj" + notification: + heading: Powiadomienia email + turn_on: Włącz + inbox: + label: Powiadomienia skrzynki odbiorczej + description: Odpowiedzi na Twoje pytania, komentarze, zaproszenia i inne. + all_new_question: + label: Wszystkie nowe pytania + description: Otrzymuj powiadomienia o wszystkich nowych pytaniach. Do 50 pytań tygodniowo. + all_new_question_for_following_tags: + label: Wszystkie nowe pytania dla obserwowanych tagów + description: Otrzymuj powiadomienia o nowych pytaniach do obserwowanych tagów. + account: + heading: Konto + change_email_btn: Zmień adres e-mail + change_pass_btn: Zmień hasło + change_email_info: >- + Wysłaliśmy e-mail na ten adres. Prosimy postępować zgodnie z instrukcjami potwierdzającymi. + email: + label: Email + new_email: + label: Nowy Email + msg: Nowy Email nie może być pusty. + pass: + label: Aktualne hasło + msg: Hasło nie może być puste. + password_title: Hasło + current_pass: + label: Aktualne hasło + msg: + empty: Obecne hasło nie może być puste. + length: Długość musi wynosić od 8 do 32 znaków. + different: Dwa wprowadzone hasła nie są zgodne. + new_pass: + label: Nowe hasło + pass_confirm: + label: Potwierdź nowe hasło + interface: + heading: Interfejs + lang: + label: Język Interfejsu + text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony. + my_logins: + title: Moje logowania + label: Zaloguj się lub zarejestruj na tej stronie za pomocą tych kont. + modal_title: Usuń logowanie + modal_content: Czy na pewno chcesz usunąć to logowanie z Twojego konta? + modal_confirm_btn: Usuń + remove_success: Pomyślnie usunięto + toast: + update: pomyślnie zaktualizowane + update_password: Hasło zostało pomyślnie zmienione. + flag_success: Dzięki za zgłoszenie. + forbidden_operate_self: Zakazane działanie na sobie + review: Twoja poprawka zostanie wyświetlona po zatwierdzeniu. + sent_success: Wysyłanie zakończone powodzeniem + related_question: + title: Related + answers: odpowiedzi + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Ludzie pytali + desc: Wybierz osoby, które mogą znać odpowiedź. + invite: Zaproś do odpowiedzi + add: Dodaj osoby + search: Wyszukaj osoby + question_detail: + action: Akcja + Asked: Zadane + asked: zadał(a) + update: Zmodyfikowane + edit: edytowany + commented: skomentowano + Views: Wyświetlone + Follow: Obserwuj + Following: Obserwuje + follow_tip: Obserwuj to pytanie, aby otrzymywać powiadomienia + answered: odpowiedziano + closed_in: Zamknięte za + show_exist: Pokaż istniejące pytanie. + useful: Przydatne + question_useful: Jest przydatne i jasne + question_un_useful: Jest niejasne lub nieprzydatne + question_bookmark: Dodaj do zakładek to pytanie + answer_useful: Jest przydatna + answer_un_useful: To nie jest użyteczne + answers: + title: Odpowiedzi + score: Ocena + newest: Najnowsze + oldest: Najstarsze + btn_accept: Akceptuj + btn_accepted: Zaakceptowane + write_answer: + title: Twoja odpowiedź + edit_answer: Edytuj moją obecną odpowiedź + btn_name: Wyślij swoją odpowiedź + add_another_answer: Dodaj kolejną odpowiedź + confirm_title: Kontynuuj odpowiedź + continue: Kontynuuj + confirm_info: >- +

Czy na pewno chcesz dodać kolejną odpowiedź?

Możesz zamiast tego użyć linku edycji, aby udoskonalić i poprawić istniejącą odpowiedź.

+ empty: Odpowiedź nie może być pusta. + characters: Treść musi mieć co najmniej 6 znaków. + tips: + header_1: Dziękujemy za Twoją odpowiedź + li1_1: Prosimy, upewnij się, że odpowiadasz na pytanie. Podaj szczegóły i podziel się swoimi badaniami. + li1_2: Popieraj swoje stwierdzenia referencjami lub osobistym doświadczeniem. + header_2: Ale unikaj ... + li2_1: Prośby o pomoc, pytania o wyjaśnienie lub odpowiadanie na inne odpowiedzi. + reopen: + confirm_btn: Ponowne otwarcie + title: Otwórz ponownie ten post + content: Czy na pewno chcesz go ponownie otworzyć? + list: + confirm_btn: Lista + title: Pokaż ten post + content: Are you sure you want to list? + unlist: + confirm_btn: Usuń z listy + title: Usuń ten post z listy + content: Czy na pewno chcesz usunąć z listy? + pin: + title: Przypnij ten post + content: Czy na pewno chcesz przypiąć go globalnie? Ten post będzie wyświetlany na górze wszystkich list postów. + confirm_btn: Przypnij + delete: + title: Usuń ten post + question: >- + Nie zalecamy usuwanie pytań wraz z udzielonymi, ponieważ pozbawia to przyszłych czytelników tej wiedzy.

Powtarzające się usuwanie pytań z odpowiedziami może skutkować zablokowaniem Twojego konta w zakresie zadawania pytań. Czy na pewno chcesz usunąć? + answer_accepted: >- +

Nie zalecamy usuwania zaakceptowanych już odpowiedzi, ponieważ pozbawia to przyszłych czytelników tej wiedzy.

Powtarzające się usuwanie zaakceptowanych odpowiedzi może skutkować zablokowaniem Twojego konta w zakresie udzielania odpowiedzi. Czy na pewno chcesz usunąć? + other: Czy na pewno chcesz usunąć? + tip_answer_deleted: Ta odpowiedź została usunięta + undelete_title: Cofnij usunięcie tego posta + undelete_desc: Czy na pewno chcesz cofnąć usunięcie? + btns: + confirm: Potwierdź + cancel: Anuluj + edit: Edytuj + save: Zapisz + delete: Usuń + undelete: Przywróć + list: Lista + unlist: Usuń z listy + unlisted: Usunięte z listy + login: Zaloguj się + signup: Zarejestruj się + logout: Wyloguj się + verify: Zweryfikuj + create: Create + approve: Zatwierdź + reject: Odrzuć + skip: Pominięcie + discard_draft: Odrzuć szkic + pinned: Przypięte + all: Wszystkie + question: Pytanie + answer: Odpowiedź + comment: Komentarz + refresh: Odśwież + resend: Wyślij ponownie + deactivate: Deaktywuj + active: Aktywne + suspend: Zawieś + unsuspend: Cofnij zawieszenie + close: Zamknij + reopen: Otwórz ponownie + ok: Ok + light: Jasny + dark: Ciemny + system_setting: Ustawienia systemowe + default: Domyślne + reset: Reset + tag: Tag + post_lowercase: wpis + filter: Filtry + ignore: Ignoruj + submit: Prześlij + normal: Normalny + closed: Zamknięty + deleted: Usunięty + deleted_permanently: Deleted permanently + pending: Oczekujący + more: Więcej + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Wyniki wyszukiwania + keywords: Słowa kluczowe + options: Opcje + follow: Obserwuj + following: Obserwuje + counts: "Liczba wyników: {{count}}" + counts_loading: "... Results" + more: Więcej + sort_btns: + relevance: Relewantność + newest: Najnowsze + active: Aktywne + score: Ocena + more: Więcej + tips: + title: Porady dotyczące wyszukiwania zaawansowanego + tag: "<1>[tag] search with a tag" + user: "<1>user:username wyszukiwanie według autora" + answer: "<1> answers:0 pytania bez odpowiedzi" + score: "<1>score:3 posty z oceną 3+" + question: "<1>is:question wyszukiwanie pytań" + is_answer: "<1>is:answer wyszukiwanie odpowiedzi" + empty: Nie mogliśmy niczego znaleźć.
Wypróbuj inne lub mniej konkretne słowa kluczowe. + share: + name: Udostępnij + copy: Skopiuj link + via: Udostępnij post za pośrednictwem... + copied: Skopiowano + facebook: Udostępnij na Facebooku + twitter: Share to X + cannot_vote_for_self: Nie możesz głosować na własne posty. + modal_confirm: + title: Błąd... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Twoje nowe konto zostało potwierdzone; zostaniesz przekierowany na stronę główną. + link: Kontynuuj do strony głównej + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Twój adres e-mail został zaktualizowany. + confirm_new_email_invalid: >- + Przepraszamy, ten link potwierdzający jest już nieaktualny. Być może twój adres e-mail został już zmieniony? + unsubscribe: + page_title: Wypisz się + success_title: Pomyślne anulowanie subskrypcji + success_desc: Zostałeś pomyślnie usunięty z listy subskrybentów i nie będziesz otrzymywać dalszych wiadomości e-mail od nas. + link: Zmień ustawienia + question: + following_tags: Obserwowane tagi + edit: Edytuj + save: Zapisz + follow_tag_tip: Obserwuj tagi, aby dostosować listę pytań. + hot_questions: Gorące pytania + all_questions: Wszystkie pytania + x_questions: "{{ count }} pytań" + x_answers: "{{ count }} odpowiedzi" + x_posts: "{{ count }} Posts" + questions: Pytania + answers: Odpowiedzi + newest: Najnowsze + active: Aktywne + hot: Gorące + frequent: Frequent + recommend: Polecane + score: Ocena + unanswered: Bez odpowiedzi + modified: zmodyfikowane + answered: udzielone odpowiedzi + asked: zadane + closed: zamknięte + follow_a_tag: Podążaj za tagiem + more: Więcej + personal: + overview: Przegląd + answers: Odpowiedzi + answer: odpowiedź + questions: Pytania + question: pytanie + bookmarks: Zakładki + reputation: Reputacja + comments: Komentarze + votes: Głosy + badges: Odznaczenia + newest: Najnowsze + score: Ocena + edit_profile: Edytuj Profil + visited_x_days: "Odwiedzone przez {{ count }} dni" + viewed: Wyświetlone + joined: Dołączył + comma: "," + last_login: Widziano + about_me: O mnie + about_me_empty: "// Hello, World !" + top_answers: Najlepsze odpowiedzi + top_questions: Najlepsze pytania + stats: Statystyki + list_empty: Nie znaleziono wpisów.
Być może chcesz wybrać inną kartę? + content_empty: No posts found. + accepted: Zaakceptowane + answered: Udzielone odpowiedzi + asked: zapytano + downvoted: oceniono negatywnie + mod_short: MOD + mod_long: Moderatorzy + x_reputation: reputacja + x_votes: otrzymane głosy + x_answers: odpowiedzi + x_questions: pytania + recent_badges: Recent Badges + install: + title: Instalacja + next: Dalej + done: Zakończono + config_yaml_error: Nie można utworzyć pliku config.yaml. + lang: + label: Wybierz język + db_type: + label: Silnik bazy danych + db_username: + label: Nazwa użytkownika + placeholder: root + msg: Nazwa użytkownika nie może być pusta. + db_password: + label: Hasło + placeholder: turbo-tajne-hasło + msg: Hasło nie może być puste. + db_host: + label: Host bazy danych (ewentualnie dodatkowo port) + placeholder: "db.domena:3306" + msg: Host bazy danych nie może być pusty. + db_name: + label: Nazwa bazy danych + placeholder: odpowiedź + msg: Nazwa bazy danych nie może być pusta. + db_file: + label: Plik bazy danych + placeholder: /data/answer.db + msg: Plik bazy danych nie może być pusty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Utwórz plik config.yaml + label: Plik config.yaml utworzony. + desc: >- + Możesz ręcznie utworzyć plik <1>config.yaml w katalogu <1>/var/wwww/xxx/ i wkleić do niego poniższy tekst. + info: Gdy już to zrobisz, kliknij przycisk "Dalej". + site_information: Informacje o witrynie + admin_account: Konto administratora + site_name: + label: Nazwa witryny + msg: Nazwa witryny nie może być pusta. + msg_max_length: Nazwa witryny musi mieć maksymalnie 30 znaków. + site_url: + label: Adres URL + text: Adres twojej strony. + msg: + empty: Adres URL nie może być pusty. + incorrect: Niepoprawny format adresu URL. + max_length: Adres URL witryny musi mieć maksymalnie 512 znaków. + contact_email: + label: Email kontaktowy + text: Email do osób odpowiedzialnych za tą witrynę. + msg: + empty: Email kontaktowy nie może być pusty. + incorrect: Email do kontaktu ma niepoprawny format. + login_required: + label: Prywatne + switch: Wymagane logowanie + text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy. + admin_name: + label: Imię + msg: Imię nie może być puste. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Hasło + text: >- + Będziesz potrzebować tego hasła do logowania. Przechowuj je w bezpiecznym miejscu. + msg: Hasło nie może być puste. + msg_min_length: Hasło musi mieć co najmniej 8 znaków. + msg_max_length: Hasło musi mieć maksymalnie 32 znaki. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: Będziesz potrzebować tego adresu e-mail do logowania. + msg: + empty: Adres e-mail nie może być pusty. + incorrect: Niepoprawny format adresu e-mail. + ready_title: Twoja strona jest gotowa + ready_desc: >- + Jeśli kiedykolwiek zechcesz zmienić więcej ustawień, odwiedź <1>sekcję administratora; znajdziesz ją w menu strony. + good_luck: "Baw się dobrze i powodzenia!" + warn_title: Ostrzeżenie + warn_desc: >- + Plik <1>config.yaml już istnieje. Jeśli chcesz zresetować którekolwiek z elementów konfiguracji w tym pliku, proszę go najpierw usunąć. + install_now: Możesz teraz <1>rozpocząć instalację. + installed: Już zainstalowane + installed_desc: >- + Wygląda na to, że masz już zainstalowaną instancję Answer. Aby zainstalować ponownie, proszę najpierw wyczyścić tabele bazy danych. + db_failed: Połączenie z bazą danych nie powiodło się + db_failed_desc: >- + Oznacza to, że informacje o bazie danych w pliku <1>config.yaml są nieprawidłowe lub że nie można nawiązać połączenia z serwerem bazy danych. Może to oznaczać, że serwer bazy danych wskazanego hosta nie działa. + counts: + views: widoki + votes: głosów + answers: odpowiedzi + accepted: Zaakceptowane + page_error: + http_error: Błąd HTTP {{ code }} + desc_403: Nie masz uprawnień do dostępu do tej strony. + desc_404: Niestety, ta strona nie istnieje. + desc_50X: Serwer napotkał błąd i nie mógł zrealizować twojego żądania. + back_home: Powrót do strony głównej + page_maintenance: + desc: "Trwa konserwacja, wrócimy wkrótce." + nav_menus: + dashboard: Panel kontrolny + contents: Zawartość + questions: Pytania + answers: Odpowiedzi + users: Użytkownicy + badges: Badges + flags: Flagi + settings: Ustawienia + general: Ogólne + interface: Interfejs + smtp: SMTP + branding: Marka + legal: Prawne + write: Pisanie + tos: Warunki korzystania z usługi + privacy: Prywatność + seo: SEO + customize: Dostosowywanie + themes: Motywy + login: Logowanie + privileges: Uprawnienia + plugins: Wtyczki + installed_plugins: Zainstalowane wtyczki + apperance: Appearance + website_welcome: Witamy w serwisie {{site_name}} + user_center: + login: Zaloguj się + qrcode_login_tip: Zeskanuj kod QR za pomocą {{ agentName }} i zaloguj się. + login_failed_email_tip: Logowanie nie powiodło się, przed ponowną próbą zezwól na dostęp tej aplikacji do informacji o Twojej skrzynce pocztowej. + badges: + modal: + title: Gratulacje + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Administrator + dashboard: + title: Panel + welcome: Witaj Administratorze! + site_statistics: Statystyki witryny + questions: "Pytania:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Odpowiedzi:" + comments: "Komentarze:" + votes: "Głosy:" + users: "Użytkownicy:" + flags: "Flagi:" + reviews: "Reviews:" + site_health: Site health + version: "Wersja:" + https: "HTTPS:" + upload_folder: "Prześlij folder:" + run_mode: "Tryb pracy:" + private: Prywatne + public: Publiczne + smtp: "SMTP:" + timezone: "Strefa czasowa:" + system_info: Informacje o systemie + go_version: "Wersja Go:" + database: "Baza danych:" + database_size: "Wielkość bazy danych:" + storage_used: "Wykorzystane miejsce:" + uptime: "Czas pracy:" + links: Linki + plugins: Wtyczki + github: GitHub + blog: Blog + contact: Kontakt + forum: Forum + documents: Dokumenty + feedback: Opinie + support: Wsparcie + review: Przegląd + config: Konfiguracja + update_to: Zaktualizuj do + latest: Najnowszej + check_failed: Sprawdzanie nie powiodło się + "yes": "Tak" + "no": "Nie" + not_allowed: Nie dozwolone + allowed: Dozwolone + enabled: Włączone + disabled: Wyłączone + writable: Zapis i odczyt + not_writable: Nie można zapisać + flags: + title: Flagi + pending: Oczekujące + completed: Zakończone + flagged: Oznaczone + flagged_type: Oznaczone {{ type }} + created: Utworzone + action: Akcja + review: Przegląd + user_role_modal: + title: Zmień rolę użytkownika na... + btn_cancel: Anuluj + btn_submit: Wyślij + new_password_modal: + title: Ustaw nowe hasło + form: + fields: + password: + label: Hasło + text: Użytkownik zostanie wylogowany i musi zalogować się ponownie. + msg: Hasło musi mieć od 8 do 32 znaków. + btn_cancel: Anuluj + btn_submit: Prześlij + edit_profile_modal: + title: Edytuj profil + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Nazwa + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Błędny adresy email. + edit_success: Edycja zakończona pomyślnie + btn_cancel: Anuluj + btn_submit: Prześlij + user_modal: + title: Dodaj nowego użytkownika + form: + fields: + users: + label: Masowo dodaj użytkownika + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Oddziel "nazwa, e-mail, hasło" przecinkami. Jeden użytkownik na linię. + msg: "Podaj adresy e-mail użytkowników, jeden w każdej linii." + display_name: + label: Nazwa wyświetlana + msg: Display name must be 2-30 characters in length. + email: + label: E-mail + msg: Email nie jest prawidłowy. + password: + label: Hasło + msg: Hasło musi mieć od 8 do 32 znaków. + btn_cancel: Anuluj + btn_submit: Prześlij + users: + title: Użytkownicy + name: Imię + email: E-mail + reputation: Reputacja + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Rola + action: Akcja + change: Zmień + all: Wszyscy + staff: Personel + more: Więcej + inactive: Nieaktywni + suspended: Zawieszeni + deleted: Usunięci + normal: Normalni + Moderator: Moderator + Admin: Administrator + User: Użytkownik + filter: + placeholder: "Filtruj według imienia, użytkownik:id" + set_new_password: Ustaw nowe hasło + edit_profile: Edytuj profil + change_status: Zmień status + change_role: Zmień rolę + show_logs: Pokaż logi + add_user: Dodaj użytkownika + deactivate_user: + title: Dezaktywuj użytkownika + content: Nieaktywny użytkownik musi ponownie potwierdzić swój adres email. + delete_user: + title: Usuń tego użytkownika + content: Czy na pewno chcesz usunąć tego użytkownika? Ta operacja jest nieodwracalna! + remove: Usuń zawartość + label: Usuń wszystkie pytania, odpowiedzi, komentarze itp. + text: Nie zaznaczaj tego, jeśli chcesz usunąć tylko konto użytkownika. + suspend_user: + title: Zawieś tego użytkownika + content: Zawieszony użytkownik nie może się logować. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Pytania + unlisted: Unlisted + post: Wpis + votes: Głosy + answers: Odpowiedzi + created: Utworzone + status: Status + action: Akcja + change: Zmień + pending: Oczekuje + filter: + placeholder: "Filtruj według tytułu, pytanie:id" + answers: + page_title: Odpowiedzi + post: Wpis + votes: Głosy + created: Utworzone + status: Status + action: Akcja + change: Zmień + filter: + placeholder: "Filtruj według tytułu, odpowiedź:id" + general: + page_title: Ogólne + name: + label: Nazwa witryny + msg: Nazwa strony nie może być pusta. + text: "Nazwa tej strony, używana w tagu tytułu." + site_url: + label: URL strony + msg: Adres Url witryny nie może być pusty. + validate: Podaj poprawny adres URL. + text: Adres Twojej strony. + short_desc: + label: Krótki opis witryny + msg: Krótki opis strony nie może być pusty. + text: "Krótki opis, używany w tagu tytułu na stronie głównej." + desc: + label: Opis witryny + msg: Opis strony nie może być pusty. + text: "Opisz tę witrynę w jednym zdaniu, użytym w znaczniku meta description." + contact_email: + label: Email kontaktowy + msg: Email kontaktowy nie może być pusty. + validate: Email kontaktowy nie jest poprawny. + text: Adres email głównego kontaktu odpowiedzialnego za tę stronę. + check_update: + label: Aktualizacjia oprogramowania + text: Automatycznie sprawdzaj dostępność aktualizacji + interface: + page_title: Interfejs + language: + label: Język Interfejsu + msg: Język interfejsu nie może być pusty. + text: Język interfejsu użytkownika. Zmieni się po odświeżeniu strony. + time_zone: + label: Strefa czasowa + msg: Strefa czasowa nie może być pusta. + text: Wybierz miasto w tej samej strefie czasowej, co Ty. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Email nadawcy + msg: Email nadawcy nie może być pusty. + text: Adres email, z którego są wysyłane wiadomości. + from_name: + label: Nazwa w polu Od + msg: Nazwa nadawcy nie może być pusta. + text: Nazwa, z której są wysyłane wiadomości. + smtp_host: + label: Serwer SMTP + msg: Host SMTP nie może być pusty. + text: Twój serwer poczty. + encryption: + label: Szyfrowanie + msg: Szyfrowanie nie może być puste. + text: Dla większości serwerów zalecana jest opcja SSL. + ssl: SSL + tls: TLS + none: Brak + smtp_port: + label: Port SMTP + msg: Port SMTP musi być liczbą od 1 do 65535. + text: Port Twojego serwera poczty. + smtp_username: + label: Nazwa użytkownika SMTP + msg: Nazwa użytkownika SMTP nie może być pusta. + smtp_password: + label: Hasło SMTP + msg: Hasło SMTP nie może być puste. + test_email_recipient: + label: Odbiorcy testowych wiadomości email + text: Podaj adres email, który otrzyma testowe wiadomości. + msg: Odbiorcy testowych wiadomości email są nieprawidłowi + smtp_authentication: + label: Wymagaj uwierzytelniania + title: Uwierzytelnianie SMTP + msg: Uwierzytelnianie SMTP nie może być puste. + "yes": "Tak" + "no": "Nie" + branding: + page_title: Marka + logo: + label: Logo + msg: Logo nie może być puste. + text: Obrazek logo znajdujący się na górze lewej strony Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli i proporcjach większych niż 3:1. Jeśli zostanie puste, wyświetlony zostanie tekst tytułu strony. + mobile_logo: + label: Logo mobilne + text: Logo używane w wersji mobilnej Twojej strony. Użyj szerokiego prostokątnego obrazka o wysokości 56 pikseli. Jeśli zostanie puste, użyty zostanie obrazek z ustawienia "logo". + square_icon: + label: Kwadratowa ikona + msg: Kwadratowa ikona nie może być pusta. + text: Obrazek używany jako podstawa dla ikon metadanych. Powinien mieć idealnie większe wymiary niż 512x512 pikseli. + favicon: + label: Ikona (favicon) + text: Ulubiona ikona witryny. Aby działać poprawnie przez CDN, musi to być png. Rozmiar zostanie zmieniony na 32x32. Jeśli pozostanie puste, zostanie użyta "kwadratowa ikona". + legal: + page_title: Prawne + terms_of_service: + label: Warunki korzystania z usługi + text: "Możesz tutaj dodać treść regulaminu. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL." + privacy_policy: + label: Polityka prywatności + text: "Możesz tutaj dodać treść polityki prywatności. Jeśli masz dokument hostowany gdzie indziej, podaj tutaj pełny URL." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Pisanie + restrict_answer: + title: Answer write + label: Każdy użytkownik może napisać tylko jedną odpowiedź na każde pytanie + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Rekomendowane tagi + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Każde nowe pytanie musi mieć przynajmniej jeden rekomendowany tag." + reserved_tags: + label: Zarezerwowane tagi + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Link bezpośredni + text: Dostosowane struktury URL mogą poprawić użyteczność i kompatybilność w przyszłości Twoich linków. + robots: + label: robots.txt + text: To trwale zastąpi wszelkie związane z witryną ustawienia. + themes: + page_title: Motywy + themes: + label: Motywy + text: Wybierz istniejący motyw. + color_scheme: + label: Schemat kolorów + navbar_style: + label: Navbar background style + primary_color: + label: Kolor podstawowy + text: Zmodyfikuj kolory używane przez Twoje motywy. + css_and_html: + page_title: CSS i HTML + custom_css: + label: Własny CSS + text: > + + head: + label: Głowa + text: > + + header: + label: Nagłówek + text: > + + footer: + label: Stopka + text: Zostanie wstawione przed </body>. + sidebar: + label: Pasek boczny + text: Będzie wstawiony w pasku bocznym. + login: + page_title: Logowanie + membership: + title: Członkostwo + label: Zezwalaj na nowe rejestracje + text: Wyłącz, aby uniemożliwić komukolwiek tworzenie nowego konta. + email_registration: + title: Rejestracja przez email + label: Zezwalaj na rejestrację przez email + text: Wyłącz, aby uniemożliwić tworzenie nowego konta poprzez email. + allowed_email_domains: + title: Dozwolone domeny email + text: Domeny email, z których użytkownicy muszą rejestrować konta. Jeden domena na linię. Ignorowane, gdy puste. + private: + title: Prywatne + label: Wymagane logowanie + text: Dostęp do tej społeczności mają tylko zalogowani użytkownicy. + password_login: + title: Hasło logowania + label: Zezwalaj na logowanie email i hasłem + text: "OSTRZEŻENIE: Jeśli wyłączone, już się nie zalogujesz, jeśli wcześniej nie skonfigurowałeś innej metody logowania." + installed_plugins: + title: Zainstalowane wtyczki + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: Wszystkie + active: Aktywne + inactive: Nieaktywne + outdated: Przestarzałe + plugins: + label: Wtyczki + text: Wybierz istniejącą wtyczkę. + name: Nazwa + version: Wersja + status: Status + action: Akcja + deactivate: Deaktywuj + activate: Aktywuj + settings: Ustawienia + settings_users: + title: Użytkownicy + avatar: + label: Domyślny Awatar + text: Dla użytkowników bez własnego awatara. + gravatar_base_url: + label: Adres URL bazy Gravatar + text: Adres URL bazy API dostawcy Gravatar. Ignorowane, gdy puste. + profile_editable: + title: Edycja profilu + allow_update_display_name: + label: Zezwalaj użytkownikom na zmianę wyświetlanej nazwy + allow_update_username: + label: Zezwalaj użytkownikom na zmianę nazwy użytkownika + allow_update_avatar: + label: Zezwalaj użytkownikom na zmianę obrazka profilowego + allow_update_bio: + label: Zezwalaj użytkownikom na zmianę opisu + allow_update_website: + label: Zezwalaj użytkownikom na zmianę witryny + allow_update_location: + label: Zezwalaj użytkownikom na zmianę lokalizacji + privilege: + title: Uprawnienia + level: + label: Wymagany poziom reputacji + text: Wybierz reputację wymaganą dla uprawnień + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Grupa + inactive: Inactive + name: Name + show_logs: Wyświetl dzienniki + status: Status + title: Badges + form: + optional: (opcjonalne) + empty: nie może być puste + invalid: jest nieprawidłowe + btn_submit: Zapisz + not_found_props: "Nie znaleziono wymaganej właściwości {{ key }}." + select: Wybierz + page_review: + review: Przegląd + proposed: zaproponowany + question_edit: Edycja pytania + answer_edit: Edycja odpowiedzi + tag_edit: Edycja tagu + edit_summary: Podsumowanie edycji + edit_question: Edytuj pytanie + edit_answer: Edytuj odpowiedź + edit_tag: Edytuj tag + empty: Nie ma więcej zadań do przeglądu. + approve_revision_tip: Czy akceptujesz tę wersję? + approve_flag_tip: Czy akceptujesz tę flagę? + approve_post_tip: Czy zatwierdzasz ten post? + approve_user_tip: Czy zatwierdzasz tego użytkownika? + suggest_edits: Suggested edits + flag_post: Oznacz wpis + flag_user: Oznacz użytkownika + queued_post: Queued post + queued_user: Queued user + filter_label: Typ + reputation: reputacja + flag_post_type: Oznaczono ten wpis jako {{ type }}. + flag_user_type: Oznaczono tego użytkownika jako {{ type }}. + edit_post: Edytuj wpis + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: nieusunięte + deleted: usunięty + downvote: odrzucenie + upvote: głos za + accept: akceptacja + cancelled: anulowane + commented: skomentowano + rollback: wycofaj + edited: edytowany + answered: odpowiedziano + asked: zapytano + closed: zamknięty + reopened: ponownie otwarty + created: utworzony + pin: przypięty + unpin: odpięty + show: wymieniony + hide: niewidoczne + title: "Historia dla" + tag_title: "Historia dla" + show_votes: "Pokaż głosy" + n_or_a: Nie dotyczy + title_for_question: "Historia dla" + title_for_answer: "Oś czasu dla odpowiedzi na {{ tytuł }} autorstwa {{ autor }}" + title_for_tag: "Oś czasu dla tagu" + datetime: Data i czas + type: Typ + by: Przez + comment: Komentarz + no_data: "Nie mogliśmy nic znaleźć." + users: + title: Użytkownicy + users_with_the_most_reputation: Użytkownicy o najwyższej reputacji w tym tygodniu + users_with_the_most_vote: Użytkownicy, którzy oddali najwięcej głosów w tym tygodniu + staffs: Nasz personel społeczności + reputation: reputacja + votes: głosy + prompt: + leave_page: Czy na pewno chcesz opuścić stronę? + changes_not_save: Twoje zmiany mogą nie zostać zapisane. + draft: + discard_confirm: Czy na pewno chcesz odrzucić swoją wersję roboczą? + messages: + post_deleted: Ten post został usunięty. + post_cancel_deleted: This post has been undeleted. + post_pin: Ten post został przypięty. + post_unpin: Ten post został odpięty. + post_hide_list: Ten post został ukryty na liście. + post_show_list: Ten post został wyświetlony na liście. + post_reopen: Ten post został ponownie otwarty. + post_list: Ten wpis został umieszczony na liście. + post_unlist: Ten wpis został usunięty z listy. + post_pending: Twój wpis oczekuje na recenzje. Będzie widoczny po jej akceptacji. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/pt_BR.yaml b/i18n/pt_BR.yaml new file mode 100644 index 000000000..b051fddb2 --- /dev/null +++ b/i18n/pt_BR.yaml @@ -0,0 +1,1381 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Sucesso. + unknown: + other: Erro desconhecido. + request_format_error: + other: O formato da Requisição não é válido. + unauthorized_error: + other: Não autorizado. + database_error: + other: Erro no servidor de dados. + role: + name: + user: + other: Usuário + admin: + other: Administrador + moderator: + other: Moderador + description: + user: + other: Padrão sem acesso especial. + admin: + other: Possui controle total para acessar o site. + moderator: + other: Não possui controle a todas as postagens exceto configurações de administrador. + email: + other: E-mail + password: + other: Senha + email_or_password_wrong_error: + other: E-mail e Senha inválidos. + error: + admin: + email_or_password_wrong: + other: E-mail e Senha inválidos. + answer: + not_found: + other: Resposta não encontrada. + cannot_deleted: + other: Permissão insuficiente para deletar. + cannot_update: + other: Permissão insuficiente para atualizar. + comment: + edit_without_permission: + other: Não é permitido atualizar comentários. + not_found: + other: Comentário não encontrado. + cannot_edit_after_deadline: + other: O tempo para comentários expirou. + email: + duplicate: + other: E-mail já existe na base de dados. + need_to_be_verified: + other: E-mail precisa ser verificado. + verify_url_expired: + other: A URL de validação do e-mail expirou, por favor, re-envie o e-mail. + lang: + not_found: + other: Arquivo de idioma não encontrado. + object: + captcha_verification_failed: + other: Captcha inválido. + disallow_follow: + other: Você não possui permissão para seguir. + disallow_vote: + other: Você não possui permissão para votar. + disallow_vote_your_self: + other: Você não pode votar na sua própria postagem. + not_found: + other: Objeto não encontrado. + verification_failed: + other: Falha na verificação. + email_or_password_incorrect: + other: E-mail e Senha não conferem. + old_password_verification_failed: + other: Verficação de senhas antigas falhou. + new_password_same_as_previous_setting: + other: A nova senha é idêntica à senha anterior. + question: + not_found: + other: Pergunta não encontrada. + cannot_deleted: + other: Não possui permissão para deletar. + cannot_close: + other: Não possui permissão para fechar. + cannot_update: + other: Não possui permissão para atualizar.. + rank: + fail_to_meet_the_condition: + other: A classificação não atende à condição. + report: + handle_failed: + other: Falha no processamento do relatório. + not_found: + other: Relatório não encontrado. + tag: + not_found: + other: Marcador não encontada. + recommend_tag_not_found: + other: Marcador recomendado não existe. + recommend_tag_enter: + other: Por favor, insira ao menos um marcador obrigatório. + not_contain_synonym_tags: + other: Não pode contar marcadores de sinônimos. + cannot_update: + other: Não possui permissão para atualizar. + cannot_set_synonym_as_itself: + other: Você não pode definir o sinônimo do marcador atual como ela mesma. + smtp: + config_from_name_cannot_be_email: + other: O De Nome não pode ser um endereço de e-mail. + Tema: + not_found: + other: Tema não encontrado. + revision: + review_underway: + other: Não é possível editar no momento, há uma versão na fila de revisão. + no_permission: + other: Sem pemissão para revisar. + user: + email_or_password_wrong: + other: + other: E-mail e senha não correspondem. + not_found: + other: Usuário não encontrado. + suspended: + other: Usuário foi suspenso. + username_invalid: + other: Nome de usuário inválido. + username_duplicate: + other: Nome de usuário já está em uso. + set_avatar: + other: Falha ao configurar Avatar. + cannot_update_your_role: + other: Você não pode modificar a sua função. + not_allowed_registration: + other: Atualmente o site não está aberto para cadastro + config: + read_config_failed: + other: A leitura da configuração falhou + database: + connection_failed: + other: Falha na conexão com o banco de dados + create_table_failed: + other: Falha na criação de tabela + install: + create_config_failed: + other: Não foi possível criar o arquivo config.yaml. + upload: + unsupported_file_format: + other: Formato de arquivo não suportado. + report: + spam: + name: + other: spam + desc: + other: Este post é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual. + rude: + name: + other: rude ou abusivo + desc: + other: Uma pessoa razoável acharia este conteúdo impróprio para um discurso respeitoso. + duplicate: + name: + other: uma duplicidade + desc: + other: Esta pergunta já foi feita antes e já tem uma resposta. + not_answer: + name: + other: não é uma resposta + desc: + other: Isso foi postado como uma resposta, mas não tenta responder à pergunta. Possivelmente deve ser uma edição, um comentário, outra pergunta ou excluído completamente. + not_need: + name: + other: não é mais necessário + desc: + other: Este comentário está desatualizado, coloquial ou não é relevante para esta postagem. + other: + name: + other: algo mais + desc: + other: Esta postagem requer atenção da equipe por outro motivo não listado acima. + question: + close: + duplicate: + name: + other: spam + desc: + other: Esta pergunta já foi feita antes e já tem uma resposta. + guideline: + name: + other: um motivo específico da comunidade + desc: + other: Esta pergunta não atende a uma diretriz da comunidade. + multiple: + name: + other: precisa de detalhes ou clareza + desc: + other: Esta pergunta atualmente inclui várias perguntas em uma. Deve se concentrar em apenas um problema. + other: + name: + other: algo mais + desc: + other: Este post requer outro motivo não listado acima. + operation_type: + asked: + other: perguntado + answered: + other: respondido + modified: + other: modificado + notification: + action: + update_question: + other: pergunta atualizada + answer_the_question: + other: pergunta respondida + update_answer: + other: resposta atualizada + accept_answer: + other: resposta aceita + comment_question: + other: pergunta comentada + comment_answer: + other: resposta comentada + reply_to_you: + other: respondeu a você + mention_you: + other: mencionou você + your_question_is_closed: + other: A sua pergunta foi fechada + your_question_was_deleted: + other: A sua pergunta foi deletada + your_answer_was_deleted: + other: A sua resposta foi deletada + your_comment_was_deleted: + other: O seu comentário foi deletado +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Como formatar + desc: >- + + pagination: + prev: Anterior + next: Próximo + page_title: + question: Pergunta + questions: Perguntas + tag: Marcador + tags: Marcadores + tag_wiki: marcador wiki + edit_tag: Editar Marcador + ask_a_question: Adicionar Pergunta + edit_question: Editar Pergunta + edit_answer: Editar Resposta + search: Busca + posts_containing: Postagens contendo + settings: Configurações + notifications: Notificações + login: Entrar + sign_up: Criar conta + account_recovery: Recuperação de conta + account_activation: Ativação de conta + confirm_email: Confirmação de e-mail + account_suspended: Conta suspensa + admin: Administrador + change_email: Modificar e-mail + install: Instalação do Resposta + upgrade: Atualização do Resposta + maintenance: Manutençã do Website + users: Usuários + notifications: + title: Notificações + inbox: Caixa de entrada + achievement: Conquistas + all_read: Marcar todas como lidas + show_more: Mostrar mais + suspended: + title: A sua conta foi suspensa + until_time: "A sua conta foi suspensa até {{ time }}." + forever: Este usuário foi suspenso por tempo indeterminado. + end: Você não atende a uma diretriz da comunidade. + editor: + blockquote: + text: Bloco de citação + bold: + text: Forte + chart: + text: Gráfico + flow_chart: Fluxograma + sequence_diagram: Diagrama de sequência + class_diagram: Diagrama de classe + state_diagram: Diagrama de estado + entity_relationship_diagram: Diagrama de relacionamento de entidade + user_defined_diagram: Diagrama definido pelo usuário + gantt_chart: Diagrama de Gantt + pie_chart: Gráfico de pizza + code: + text: Código de exemplo + add_code: Adicione código de exemplo + form: + fields: + code: + label: Código + msg: + empty: Código não pode ser vazio. + language: + label: Idioma (opcional) + placeholder: Tetecção automática + btn_cancel: Cancelar + btn_confirm: Adicionar + formula: + text: Fórmula + options: + inline: Fórmula em linha + block: Fórmula em bloco + Cabeçalho: + text: Cabeçalho + options: + h1: Cabeçalho 1 + h2: Cabeçalho 2 + h3: Cabeçalho 3 + h4: Cabeçalho 4 + h5: Cabeçalho 5 + h6: Cabeçalho 6 + help: + text: Ajuda + hr: + text: Régua horizontal + image: + text: Imagem + add_image: Adicionar imagem + tab_image: Enviar image, + form_image: + fields: + file: + label: Arquivo de imagem + btn: Selecione imagem + msg: + empty: Arquivo não pode ser vazio. + only_image: Somente um arquivo de imagem é permitido. + max_size: O tamanho do arquivo não pode exceder 4 MB. + desc: + label: Descrição (opcional) + tab_url: URL da imagem + form_url: + fields: + url: + label: URL da imagem + msg: + empty: URL da imagem não pode ser vazia. + name: + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar + uploading: Enviando + indent: + text: Identação + outdent: + text: Não identado + italic: + text: Emphase + link: + text: Superlink (Hyperlink) + add_link: Adicionar superlink (hyperlink) + form: + fields: + url: + label: URL + msg: + empty: URL não pode ser vazia. + name: + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar + ordered_list: + text: Lista numerada + unordered_list: + text: Lista com marcadores + table: + text: Tabela + Cabeçalho: Cabeçalho + cell: Célula + close_modal: + title: Estou fechando este post como... + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: Não pode ser vazio. + msg: + empty: Por favor selecione um motivo. + report_modal: + flag_title: I am flagging to report this post as... + close_title: Estou fechando este post como... + review_question_title: Revisar pergunta + review_answer_title: Revisar resposta + review_comment_title: Revisar comentário + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: Não pode ser vazio. + msg: + empty: Por favor selecione um motivo. + tag_modal: + title: Criar novo marcador + form: + fields: + display_name: + label: Nome de exibição + msg: + empty: Nome de exibição não pode ser vazio. + range: Nome de exibição tem que ter até 35 caracteres. + slug_name: + label: Slug de URL + desc: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug não pode ser vazio. + range: URL slug até 35 caracteres. + character: URL slug contém conjunto de caracteres não permitido. + desc: + label: Descrição (opcional) + btn_cancel: Cancelar + btn_submit: Enviar + tag_info: + created_at: Criado + edited_at: Editado + history: Histórico + synonyms: + title: Sinônimos + text: Os marcadores a seguir serão re-mapeados para + empty: Sinônimos não encotrados. + btn_add: Adicionar um sinônimo + btn_edit: Editar + btn_save: Salvar + synonyms_text: Os marcadores a seguir serão re-mapeados para + delete: + title: Deletar este marcador + content: >- +

Não permitimos a exclusão de marcadores com postagens.

Por favor, remova este marcador das postagens primeiro.

+ content2: Você tem certeza que deseja deletar? + close: Fechar + edit_tag: + title: Editar marcador + default_reason: Editar marcador + form: + fields: + revision: + label: Revisão + display_name: + label: Nome de exibição + slug_name: + label: Slug de URL + info: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' + desc: + label: Descrição + edit_summary: + label: Resumo da edição + placeholder: >- + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_save_edits: Salvar edições + btn_cancel: Cancelar + dates: + long_date: D MMM + long_date_with_year: "D MMM, YYYY" + long_date_with_time: "D MMM, YYYY [at] HH:mm" + now: agora + x_seconds_ago: "{{count}}s atrás" + x_minutes_ago: "{{count}}m atrás" + x_hours_ago: "{{count}}h atrás" + hour: hora + day: dia + comment: + btn_add_comment: Adicionar comentário + reply_to: Responder a + btn_reply: Responder + btn_edit: Editar + btn_delete: Deletar + btn_flag: Marcador + btn_save_edits: Salvar edições + btn_cancel: Cancelar + show_more: Mostrar mais comentários + tip_question: >- + Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários. + tip_answer: >- + Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar. + edit_answer: + title: Editar Resposta + default_reason: Editar Resposta + form: + fields: + revision: + label: Revisão + answer: + label: Resposta + feedback: + caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. + edit_summary: + label: Resumo da edição + placeholder: >- + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_save_edits: Salvar edições + btn_cancel: Cancelar + tags: + title: Marcadores + sort_buttons: + popular: Popular + name: Nome + newest: mais recente + button_follow: Seguir + button_following: Seguindo + tag_label: perguntas + search_placeholder: Filtrar por nome de marcador + no_desc: O marcador não possui descrição. + more: Mais + ask: + title: Adicionar Pergunta + edit_title: Editar Pergunta + default_reason: Editar pergunta + similar_questions: Similar perguntas + form: + fields: + revision: + label: Revisão + title: + label: Título + placeholder: Seja específico e tenha em mente que está fazendo uma pergunta a outra pessoa + msg: + empty: Título não pode ser vazio. + range: Título até 150 caracteres + body: + label: Corpo + msg: + empty: Corpo da mensagem não pode ser vazio. + tags: + label: Marcadores + msg: + empty: Marcadores não podes ser vazios. + answer: + label: Resposta + msg: + empty: Resposta não pode ser vazia. + edit_summary: + label: Resumo da edição + placeholder: >- + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_post_question: Publicação a sua pergunta + btn_save_edits: Salvar edições + answer_question: Responda a sua própria pergunta + post_question&answer: Publicação a sua pergunta e resposta + tag_selector: + add_btn: Adicionar marcador + create_btn: Criar novo marcador + search_tag: Procurar marcador + hint: "Descreva do quê se trata a sua pergunta, ao menos um marcador é requirido." + no_result: Nenhum marcador correspondente + tag_required_text: Marcador obrigatório (ao menos um) + header: + nav: + question: Perguntas + tag: Marcadores + user: Usuários + profile: Perfil + setting: Configurações + logout: Sair + admin: Administrador + review: Revisar + search: + placeholder: Procurar + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Mudar + loading: carregando... + pic_auth_code: + title: Captcha + placeholder: Escreva o texto acima + msg: + empty: Captcha não pode ser vazio. + inactive: + first: >- + Você está quase pronto! Enviamos um e-mail de ativação para {{mail}}. Por favor, siga as instruções no e-mail para ativar uma conta sua. + info: "Se não chegar, verifique sua pasta de spam." + another: >- + Enviamos outro e-mail de ativação para você em {{mail}}. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam. + btn_name: Resend activation email + change_btn_name: Mudar email + msg: + empty: Não pode ser vazio. + login: + page_title: Bem vindo ao {{site_name}} + login_to_continue: Entre para continue + info_sign: Não possui uma conta? <1>Cadastrar-se + info_login: Já possui uma conta? <1>Entre + agreements: Ao se registrar, você concorda com as <1>políticas de privacidades e os <3>termos de serviços. + forgot_pass: Esqueceu a sua senha? + name: + label: Nome + msg: + empty: Nome não pode ser vazio. + range: Nome até 30 caracteres. + email: + label: E-mail + msg: + empty: E-mail não pode ser vazio. + password: + label: Senha + msg: + empty: Senha não pode ser vazia. + different: As senhas inseridas em ambos os campos são inconsistentes + account_forgot: + page_title: Esqueceu a sua senha + btn_name: Enviar e-mail de recuperação de senha + send_success: >- + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. + email: + label: E-mail + msg: + empty: E-mail não pode ser vazio. + change_email: + page_title: Bem vindo ao {{site_name}} + btn_cancel: Cancelar + btn_update: Atualiza email address + send_success: >- + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. + email: + label: Novo E-mail + msg: + empty: E-mail não pode ser vazio. + password_reset: + page_title: Redefinir senha + btn_name: Redefinir minha senha + reset_success: >- + Você alterou com sucesso uma senha sua; você será redirecionado para a página de login. + link_invalid: >- + Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida? + to_login: Continuar para a tela de login + password: + label: Senha + msg: + empty: Senha não pode ser vazio. + length: O comprimento deve estar entre 8 e 32 + different: As senhas inseridas em ambos os campos são inconsistentes + password_confirm: + label: Confirmar Nova senha + settings: + page_title: Configurações + nav: + profile: Perfil + notification: Notificações + account: Conta + interface: Interface + profile: + Cabeçalho: Perfil + btn_name: Salvar + display_name: + label: Nome de exibição + msg: Nome de exibição não pode ser vazio. + msg_range: Nome de exibição tem que ter até 30 caracteres. + username: + label: Nome de usuário + caption: As pessoas poderão mensionar você com "@usuário". + msg: Nome de usuário não pode ser vazio. + msg_range: Nome de usuário até 30 caracteres. + character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"' + avatar: + label: Perfil Imagem + gravatar: Gravatar + gravatar_text: Você pode mudar a imagem em <1>gravatar.com + custom: Customizado + btn_refresh: Atualizar + custom_text: Você pode enviar a sua image. + default: System + msg: Por favor envie um avatar + bio: + label: Sobre mim (opcional) + website: + label: Website (opcional) + placeholder: "https://exemplo.com.br" + msg: Formato incorreto de endereço de Website + location: + label: Localização (opcional) + placeholder: "Cidade, País" + notification: + Cabeçalho: Notificações + email: + label: E-mail Notificações + radio: "Responda as suas perguntas, comentários, e mais" + account: + Cabeçalho: Conta + change_email_btn: Mudar e-mail + change_pass_btn: Mudar senha + change_email_info: >- + Enviamos um e-mail para esse endereço. Siga as instruções de confirmação. + email: + label: E-mail + msg: E-mail não pode ser vazio. + password_title: Senha + current_pass: + label: Senha atual + msg: + empty: A Senha não pode ser vazia. + length: O comprimento deve estar entre 8 and 32. + different: As duas senhas inseridas não correspondem. + new_pass: + label: Nova Senha + pass_confirm: + label: Confirmar nova Senha + interface: + Cabeçalho: Interface + lang: + label: Idioma da Interface + text: Idioma da interface do usuário. A interface mudará quando você atualizar a página. + toast: + update: atualização realizada com sucesso + update_password: Senha alterada com sucesso. + flag_success: Obrigado por marcar. + forbidden_operate_self: Proibido para operar por você mesmo + review: A sua resposta irá aparecer após a revisão. + related_question: + title: Perguntas relacionadas + btn: Adicionar pegunta + answers: respostas + question_detail: + Perguntado: Perguntado + asked: perguntado + update: Modificado + edit: modificado + Views: Visualizado + Seguir: Seguir + Seguindo: Seguindo + answered: respondido + closed_in: Fechado em + show_exist: Mostrar pergunta existente. + answers: + title: Respostas + score: Pontuação + newest: Mais recente + btn_accept: Aceito + btn_accepted: Aceito + write_answer: + title: A sua Resposta + btn_name: Publicação a sua resposta + add_another_answer: Adicionar outra resposta + confirm_title: Continuar a responder + continue: Continuar + confirm_info: >- +

Tem certeza de que deseja adicionar outra resposta?

Você pode usar o link de edição para refinar e melhorar uma resposta existente.

+ empty: Resposta não pode ser vazio. + caracteres: conteúdo deve ser pelo menos 6 caracteres em comprimento. + reopen: + title: Reabrir esta postagem + content: Tem certeza que deseja reabrir? + success: Esta postagem foi reaberta + delete: + title: Excluir esta postagem + question: >- + Nós não recomendamos excluindo perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of answered questions can result in a sua account being blocked from asking. Você tem certeza que deseja deletar? + answer_accepted: >- +

Nós não recomendamos deleting accepted answer porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of accepted answers can result in a sua account being blocked from answering. Você tem certeza que deseja deletar? + other: Você tem certeza que deseja deletar? + tip_question_deleted: Esta postagem foi deletada + tip_answer_deleted: Esta resposta foi deletada + btns: + confirm: Confirmar + cancel: Cancelar + save: Salvar + delete: Deletar + login: Entrar + signup: Cadastrar-se + logout: Sair + verify: Verificar + add_question: Adicionar pergunta + approve: Aprovar + reject: Rejetar + skip: Pular + search: + title: Procurar Resultados + keywords: Palavras-chave + options: Opções + follow: Seguir + following: Seguindo + counts: "{{count}} Resultados" + more: Mais + sort_btns: + relevance: Relevância + newest: Mais recente + active: Ativar + score: Pontuação + more: Mais + tips: + title: Dicas de Pesquisa Avançada + tag: "<1>[tag] pesquisar dentro de um marcador" + user: "<1>user:username buscar por autor" + answer: "<1>answers:0 perguntas não respondidas" + score: "<1>score:3 postagens com mais de 3+ placares" + question: "<1>is:question buscar perguntas" + is_answer: "<1>is:answer buscar respostas" + empty: Não conseguimos encontrar nada.
Tente palavras-chave diferentes ou menos específicas. + share: + name: Compartilhar + copy: Copiar link + via: Compartilhar postagem via... + copied: Copiado + facebook: Compartilhar no Facebook + twitter: Compartilhar no X + cannot_vote_for_self: Você não pode votar na sua própria postagem + modal_confirm: + title: Erro... + account_result: + page_title: Bem vindo ao {{site_name}} + success: A sua nova conta está confirmada; você será redirecionado para a página inicial. + link: Continuar para a página inicial. + invalid: >- + Desculpe, este link de confirmação não é mais válido. Talvez a sua já está ativa. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado. + unsubscribe: + page_title: Cancelar subscrição + success_title: Cancelamento de inscrição bem-sucedido + success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso. + link: Mudar configurações + question: + following_tags: Seguindo Marcadores + edit: Editar + save: Salvar + follow_tag_tip: Seguir tags to curate a sua lista de perguntas. + hot_questions: Perguntas quentes + all_questions: Todas Perguntas + x_questions: "{{ count }} perguntas" + x_answers: "{{ count }} respostas" + questions: Perguntas + answers: Respostas + newest: Mais recente + active: Ativo + hot: Hot + score: Pontuação + unanswered: Não Respondido + modified: modificado + answered: respondido + asked: perguntado + closed: fechado + follow_a_tag: Seguir o marcador + more: Mais + personal: + overview: Visão geral + answers: Respostas + answer: resposta + questions: Perguntas + question: pergunta + bookmarks: Favoritas + reputation: Reputação + comments: Comentários + Votos: Votos + newest: Mais recente + score: Pontuação + edit_profile: Editar Perfil + visited_x_days: "Visitado {{ count }} dias" + viewed: Viewed + joined: Ingressou + last_login: Visto + about_me: Sobre mim + about_me_empty: "// Olá, Mundo !" + top_answers: Melhores Respostas + top_questions: Melhores Perguntas + stats: Estatísticas + list_empty: Postagens não encontradas.
Talvez você queira selecionar uma guia diferente? + accepted: Aceito + answered: respondido + asked: perguntado + upvote: voto positivo + downvote: voto negativo + mod_short: Moderador + mod_long: Moderadores + x_reputation: reputação + x_Votos: votos recebidos + x_answers: respostas + x_questions: perguntas + install: + title: Instalação + next: Proximo + done: Completo + config_yaml_error: Não é possível criar o arquivo config.yaml. + lang: + label: Por favor Escolha um Idioma + db_type: + label: Database Engine + db_username: + label: Nome de usuário + placeholder: root + msg: Nome de usuário não pode ser vazio. + db_password: + label: Senha + placeholder: root + msg: Senha não pode ser vazio. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host não pode ser vazio. + db_name: + label: Database Nome + placeholder: answer + msg: Database Nome não pode ser vazio. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File não pode ser vazio. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Administrador Conta + site_name: + label: Site Nome + msg: Site Nome não pode ser vazio. + site_url: + label: Site URL + text: The address of a sua site. + msg: + empty: Site URL não pode ser vazio. + incorrect: Site URL incorrect format. + contact_email: + label: E-mail par contato + text: Email address of key contact responsible for this site. + msg: + empty: E-mail par contato não pode ser vazio. + incorrect: E-mail par contato incorrect format. + admin_name: + label: Nome + msg: Nome não pode ser vazio. + admin_password: + label: Senha + text: >- + You will need this password to log in. Por favor store it in a secure location. + msg: Senha não pode ser vazio. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email não pode ser vazio. + incorrect: Email incorrect format. + ready_title: Your Resposta is Ready! + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear a sua old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in a sua <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean a sua host's database server is down. + counts: + views: visualizações + Votos: votos + answers: respostas + accepted: Aceito + page_404: + desc: "Infelizmente, esta postagem não existe mais." + back_home: Voltar para a página inicial + page_50X: + desc: O servidor encontrou um erro e não pôde concluir uma solicitação sua. + back_home: Voltar para a página inicial + page_maintenance: + desc: "Estamos em manutenção, voltaremos em breve." + nav_menus: + dashboard: Painel + contents: Conteúdos + questions: Perguntas + answers: Respostas + users: Usuários + flags: Marcadores + settings: Configurações + general: Geral + interface: Interface + smtp: SMTP + branding: Marca + legal: Legal + write: Escrever + tos: Termos de Serviços + privacy: Privacidade + seo: SEO + customize: Personalização + Temas: Temas + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Administrador + dashboard: + title: Painel + welcome: Bem vindo ao Administrador! + site_statistics: Estatísticas do Site + questions: "Perguntas:" + answers: "Respostas:" + comments: "Comentários:" + Votos: "Votos:" + active_users: "Usuários ativos:" + flags: "Marcadores:" + site_health_status: Estatísticas da saúde do site + version: "Versão:" + https: "HTTPS:" + uploading_files: "Enviando arquivos:" + smtp: "SMTP:" + timezone: "Fuso horário:" + system_info: Informações do Sistema + storage_used: "Armazenamento usado:" + uptime: "Tempo de atividade:" + answer_links: Links das Respostas + documents: Documentos + feedback: Opinião + support: Supporte + review: Revisar + config: Configurações + update_to: Atualizar ao + latest: Ultimo + check_failed: Falha na verificação + "yes": "Sim" + "no": "Não" + not_allowed: Não permitido + allowed: Permitido + enabled: Ativo + disabled: Disponível + flags: + title: Marcadores + pending: Pendente + completed: Completo + flagged: Marcado + created: Criado + action: Ação + review: Revisar + change_modal: + title: Mudar user status to... + btn_cancel: Cancelar + btn_submit: Enviar + normal_name: normal + normal_desc: Um usuário normal pode fazer e responder perguntas. + suspended_name: suspenso + suspended_desc: Um usuário suspenso não pode fazer login. + deleted_name: removido + deleted_desc: "Deletar perfil, associações de autenticação." + inactive_name: inativo + inactive_desc: Um usuário inativo deve revalidar seu e-mail. + confirm_title: Remover este usuário + confirm_content: Tem certeza de que deseja excluir este usuário? Isso é permanente! + confirm_btn: Deletar + msg: + empty: Por favor selecione um motivo. + status_modal: + title: "Mudar {{ type }} status para..." + normal_name: normal + normal_desc: Um post normal disponível para todos. + closed_name: fechado + closed_desc: "Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar." + deleted_name: removido + deleted_desc: Toda reputação ganha e perdida será restaurada. + btn_cancel: Cancelar + btn_submit: Enviar + btn_next: Próximo + user_role_modal: + title: Altere a função do usuário para... + btn_cancel: Cancelar + btn_submit: Enviar + users: + title: Usuários + name: Nome + email: Email + reputation: Reputação + created_at: Hora de criação + delete_at: Hora da remoção + suspend_at: Hora da suspensão + status: Status + role: Função + action: Ação + change: Mudar + all: Todos + staff: Funcionários + inactive: Inativo + suspended: Suspenso + deleted: Removido + normal: Normal + Moderador: Moderador + Administrador: Administrador + Usuário: Usuário + filter: + placeholder: "Filtrar por nome, user:id" + set_new_password: Configurar nova senha + change_status: Mudar status + change_role: Mudar função + show_logs: Mostrar registros + add_user: Adicionar usuário + new_password_modal: + title: Configurar nova senha + form: + fields: + password: + label: Senha + text: O usuário será desconectado e precisará fazer login novamente. + msg: Senha de ver entre 8-32 caracteres em comprimento. + btn_cancel: Cancelar + btn_submit: Enviar + user_modal: + title: Adicionar novo usuário + form: + fields: + display_name: + label: Nome de exibição + msg: Nome de exibição deve ser 2-30 caracteres em comprimento. + email: + label: Email + msg: Email não é válido. + password: + label: Senha + msg: Senha deve ser 8-32 caracteres em comprimento. + btn_cancel: Cancelar + btn_submit: Enviar + questions: + page_title: Perguntas + normal: Normal + closed: Fechado + deleted: Removido + post: Publicação + Votos: Votos + answers: Respostas + created: Criado + status: Status + action: Ação + change: Mudar + filter: + placeholder: "Filtrar por título, question:id" + answers: + page_title: Respostas + normal: Normal + deleted: Removido + post: Publicação + Votos: Votos + created: Criado + status: Status + action: Ação + change: Mudar + filter: + placeholder: "Filtrar por título, answer:id" + general: + page_title: General + name: + label: Site Nome + msg: Site name não pode ser vazio. + text: "O nome deste site, conforme usado na tag de título." + site_url: + label: URL do Site + msg: Site url não pode ser vazio. + validate: Por favor digite uma URL válida. + text: O endereço do seu site. + short_desc: + label: Breve Descrição do site (opcional) + msg: Breve Descrição do site não pode ser vazio. + text: "Breve descrição, conforme usado na tag de título na página inicial." + desc: + label: Site Descrição (opcional) + msg: Descrição do site não pode ser vazio. + text: "Descreva este site em uma única sentença, conforme usado na meta tag de descrição." + contact_email: + label: E-mail para contato + msg: E-mail par contato não pode ser vazio. + validate: E-mail par contato não é válido. + text: Endereço de e-mail do principal contato responsável por este site. + interface: + page_title: Interface + logo: + label: Logo (opcional) + msg: Site logo não pode ser vazio. + text: Você pode enviar a sua image ou <1>reiniciar ao texto do título do site. + Tema: + label: Tema + msg: Tema não pode ser vazio. + text: Selecione um tema existente. + language: + label: Idioma da interface + msg: Idioma da Interface não pode ser vazio. + text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página. + time_zone: + label: Fuso horário + msg: Fuso horário não pode ser vazio. + text: Escolha a cidade no mesmo fuso horário que você. + smtp: + page_title: SMTP + from_email: + label: E-mail de origem + msg: E-mail de origem não pode ser vazio. + text: O endereço de e-mail de onde os e-mails são enviados. + from_name: + label: Nome de origem + msg: Nome de origem não pode ser vazio. + text: O nome de onde os e-mails são enviados. + smtp_host: + label: SMTP Host + msg: SMTP host não pode ser vazio. + text: O seu servidor de e-mails. + encryption: + label: Criptografia + msg: Criptografia não pode ser vazio. + text: Para a maioria dos servidores SSL é a opção recomendada. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to a sua mail server. + smtp_username: + label: SMTP Nome de usuário + msg: SMTP username não pode ser vazio. + smtp_password: + label: SMTP Senha + msg: SMTP password não pode ser vazio. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication não pode ser vazio. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (opcional) + msg: Logo não pode ser vazio. + text: The logo image at the top left of a sua site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (opcional) + text: The logo used on mobile version of a sua site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (opcional) + msg: Square icon não pode ser vazio. + text: Imagem used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (opcional) + text: A favicon for a sua site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Marcadores + text: "Por favor input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as requirido + text: "Every new question must have ao menos one recommend tag." + reserved_tags: + label: Reserved Marcadores + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of a sua links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + Temas: + page_title: Temas + Temas: + label: Temas + text: Select an existing Tema. + navbar_style: + label: Navbar Style + text: Select an existing Tema. + primary_color: + label: Primary Color + text: Modify the colors used by a sua Temas + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login requirido + text: Only logged in users can access this community. + form: + empty: não pode ser vazio + invalid: is invalid + btn_submit: Salvar + not_found_props: "Required property {{ key }} not found." + page_review: + review: Revisar + proposed: proposed + question_edit: Pergunta edit + answer_edit: Resposta edit + tag_edit: Tag edit + edit_summary: Editar summary + edit_question: Editar question + edit_answer: Editar answer + edit_tag: Editar tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "Histórico for" + tag_title: "Timeline for" + show_Votos: "Show Votos" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Usuários + users_with_the_most_reputation: Usuários with the highest reputation scores + users_with_the_most_vote: Usuários who voted the most + staffs: Our community staff + reputation: reputation + Votos: Votos diff --git a/i18n/pt_PT.yaml b/i18n/pt_PT.yaml new file mode 100644 index 000000000..ec9609c1c --- /dev/null +++ b/i18n/pt_PT.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Sucesso. + unknown: + other: Erro desconhecido. + request_format_error: + other: Formato de solicitação inválido. + unauthorized_error: + other: Não autorizado. + database_error: + other: Erro no servidor de dados. + forbidden_error: + other: Proibido. + duplicate_request_error: + other: Solicitação duplicada. + action: + report: + other: Bandeira + edit: + other: Editar + delete: + other: Excluir + close: + other: Fechar + reopen: + other: Reabrir + forbidden_error: + other: Proibido. + pin: + other: Fixar + hide: + other: Não listar + unpin: + other: Desafixar + show: + other: Lista + invite_someone_to_answer: + other: Editar + undelete: + other: Desfazer + merge: + other: Merge + role: + name: + user: + other: Usuário + admin: + other: Administrador + moderator: + other: Moderador + description: + user: + other: Padrão sem acesso especial. + admin: + other: Possui acesso total. + moderator: + other: Acesso a todas as postagens, exceto configurações administrativas. + privilege: + level_1: + description: + other: Nível 1 (Menos reputação necessária para a equipe privado, grupo) + level_2: + description: + other: Nível 2 (pouca reputação necessária para a comunidade de inicialização) + level_3: + description: + other: Nível 3 (Alta reputação necessária para a comunidade adulta) + level_custom: + description: + other: Nível customizado + rank_question_add_label: + other: Perguntar + rank_answer_add_label: + other: Escrever resposta + rank_comment_add_label: + other: Comentar + rank_report_add_label: + other: Bandeira + rank_comment_vote_up_label: + other: Aprovar um comentário + rank_link_url_limit_label: + other: Poste mais de 2 links por vez + rank_question_vote_up_label: + other: Avaliar perguntar + rank_answer_vote_up_label: + other: Votar na resposta + rank_question_vote_down_label: + other: Desaprovar pergunta + rank_answer_vote_down_label: + other: Desaprovar resposta + rank_invite_someone_to_answer_label: + other: Convide alguém para responder + rank_tag_add_label: + other: Criar marcador + rank_tag_edit_label: + other: Editar descrição de um marcador (revisão necessária) + rank_question_edit_label: + other: Editar pergunta do outro (revisão necessária) + rank_answer_edit_label: + other: Editar a resposta do outro (revisão necessária) + rank_question_edit_without_review_label: + other: Editar a pergunta do outro sem revisar + rank_answer_edit_without_review_label: + other: Editar a resposta do outro sem revisar + rank_question_audit_label: + other: Rever edições de perguntas + rank_answer_audit_label: + other: Revisar edições de respostas + rank_tag_audit_label: + other: Revisar edições de marcadores + rank_tag_edit_without_review_label: + other: Editar descrições de marcadores sem revisar + rank_tag_synonym_label: + other: Gerenciar sinônimos de marcação + email: + other: E-mail + e_mail: + other: Email + password: + other: Senha + pass: + other: Senha + old_pass: + other: Current password + original_text: + other: Esta publicação + email_or_password_wrong_error: + other: O e-mail e a palavra-passe não coincidem. + error: + common: + invalid_url: + other: URL inválida. + status_invalid: + other: Estado inválido. + password: + space_invalid: + other: A senha não pode conter espaços. + admin: + cannot_update_their_password: + other: Você não pode modificar sua senha. + cannot_edit_their_profile: + other: Você não pode alterar o seu perfil. + cannot_modify_self_status: + other: Você não pode modificar seu status + email_or_password_wrong: + other: O e-mail e a palavra-passe não coincidem. + answer: + not_found: + other: Resposta não encontrada. + cannot_deleted: + other: Sem permissão para remover. + cannot_update: + other: Sem permissão para atualizar. + question_closed_cannot_add: + other: Perguntas são fechadas e não podem ser adicionadas. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Não é possível alterar comentários. + not_found: + other: Comentário não encontrado. + cannot_edit_after_deadline: + other: O tempo do comentário foi muito longo para ser modificado. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: O e-mail já existe. + need_to_be_verified: + other: O e-mail deve ser verificado. + verify_url_expired: + other: O e-mail verificado URL expirou, por favor, reenvie o e-mail. + illegal_email_domain_error: + other: O email não é permitido a partir desse email de domínio. Por favor, use outro. + lang: + not_found: + other: Arquivo de idioma não encontrado. + object: + captcha_verification_failed: + other: O Captcha está incorreto. + disallow_follow: + other: Você não tem permissão para seguir. + disallow_vote: + other: Você não possui permissão para votar. + disallow_vote_your_self: + other: Você não pode votar na sua própria postagem. + not_found: + other: Objeto não encontrado. + verification_failed: + other: A verificação falhou. + email_or_password_incorrect: + other: O e-mail e a senha não correspondem. + old_password_verification_failed: + other: Falha na verificação de senha antiga + new_password_same_as_previous_setting: + other: A nova senha é a mesma que a anterior. + already_deleted: + other: Esta publicação foi removida. + meta: + object_not_found: + other: Objeto meta não encontrado + question: + already_deleted: + other: Essa publicação foi deletado. + under_review: + other: Sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada. + not_found: + other: Pergunta não encontrada. + cannot_deleted: + other: Sem permissão para remover. + cannot_close: + other: Sem permissão para fechar. + cannot_update: + other: Sem permissão para atualizar. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: A classificação não atende à condição. + vote_fail_to_meet_the_condition: + other: Obrigado pela sugestão. Você precisa ser {{.Rank}} para poder votar. + no_enough_rank_to_operate: + other: Você precisa ser pelo menos {{.Rank}} para fazer isso. + report: + handle_failed: + other: Falha ao manusear relatório. + not_found: + other: Relatório não encontrado. + tag: + already_exist: + other: Marcação já existe. + not_found: + other: Marca não encontrada. + recommend_tag_not_found: + other: Marcador recomendado não existe. + recommend_tag_enter: + other: Por favor, insira pelo menos um marcador obrigatório. + not_contain_synonym_tags: + other: Não deve conter marcadores sinónimos. + cannot_update: + other: Sem permissão para atualizar. + is_used_cannot_delete: + other: Não é possível excluir um marcador em uso. + cannot_set_synonym_as_itself: + other: Você não pode definir o sinônimo do marcador atual como a si mesmo. + smtp: + config_from_name_cannot_be_email: + other: O De Nome não pode ser um endereço de e-mail. + theme: + not_found: + other: Tema não encontrado. + revision: + review_underway: + other: Não é possível editar atualmente, há uma versão na fila de análise. + no_permission: + other: Sem pemissão para revisar. + user: + external_login_missing_user_id: + other: A plataforma de terceiros não fornece um ID único de usuário, então você não pode fazer login, entre em contato com o administrador do site. + external_login_unbinding_forbidden: + other: Por favor, defina uma senha de login para sua conta antes de remover esta conta. + email_or_password_wrong: + other: + other: O e-mail e a senha não conferem. + not_found: + other: Usuário não encontrado. + suspended: + other: O utilizador foi suspenso. + username_invalid: + other: Nome de usuário inválido. + username_duplicate: + other: O nome de usuário já em uso. + set_avatar: + other: Configuração de avatar falhou. + cannot_update_your_role: + other: Você não pode modificar a sua função. + not_allowed_registration: + other: Atualmente o site não está aberto para cadastro + not_allowed_login_via_password: + other: Atualmente o site não tem permissão para acessar utilizando senha. + access_denied: + other: Acesso negado + page_access_denied: + other: Você não tem permissão de acesso para esta página. + add_bulk_users_format_error: + other: "Erro no formato {{.Field}} próximo '{{.Content}}' na linha {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "O número de usuários que você adicionou de uma vez deve estar no intervalo de 1 -{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Falha ao ler configuração + database: + connection_failed: + other: Falha ao conectar-se ao banco de dados + create_table_failed: + other: Falha ao criar tabela + install: + create_config_failed: + other: Não foi possível criar o arquivo de configuração. + upload: + unsupported_file_format: + other: Formato de arquivo não suportado. + site_info: + config_not_found: + other: Configuração do site não encontrada. + badge: + object_not_found: + other: Objeto emblema não encontrado + reason: + spam: + name: + other: spam + desc: + other: Essa postagem é um anúncio, ou vandalismo. Não é útil ou relevante para o tópico atual. + rude_or_abusive: + name: + other: rude ou abusivo + desc: + other: "Uma pessoa razoável consideraria esse conteúdo inapropriado para um discurso respeitoso." + a_duplicate: + name: + other: uma duplicação + desc: + other: Esta pergunta já foi feita antes e já possui uma resposta. + placeholder: + other: Insira o link existente para a pergunta + not_a_answer: + name: + other: não é uma resposta + desc: + other: "Foi apresentada como uma resposta, mas não tenta responder à pergunta. Talvez deva ser uma edição, um comentário, outra pergunta ou totalmente apagada." + no_longer_needed: + name: + other: não é mais necessário + desc: + other: Este comentário está desatualizado, conversacional ou não é relevante para esta publicação. + something: + name: + other: outro assunto + desc: + other: Esta postagem requer atenção da equipe por outra razão não listada acima. + placeholder: + other: Conte-nos especificamente com o que você está preocupado + community_specific: + name: + other: um motivo específico para a comunidade + desc: + other: Esta questão não atende a uma orientação da comunidade. + not_clarity: + name: + other: precisa de detalhes ou clareza + desc: + other: Atualmente esta pergunta inclui várias perguntas em um só. Deve se concentrar apenas em um problema. + looks_ok: + name: + other: parece estar OK + desc: + other: Este post é bom como está e não de baixa qualidade. + needs_edit: + name: + other: necessitava de edição, assim o fiz + desc: + other: Melhore e corrija problemas com este post você mesmo. + needs_close: + name: + other: precisa ser fechado + desc: + other: Uma pergunta fechada não pode responder, mas ainda pode editar, votar e comentar. + needs_delete: + name: + other: precisa ser excluído + desc: + other: Esta postagem será excluída. + question: + close: + duplicate: + name: + other: spam + desc: + other: Esta pergunta já foi feita antes e já tem uma resposta. + guideline: + name: + other: um motivo específico da comunidade + desc: + other: Esta pergunta não atende a uma diretriz da comunidade. + multiple: + name: + other: precisa de detalhes ou clareza + desc: + other: Esta pergunta atualmente inclui várias perguntas em uma só. Por isso, deve focar em apenas um problema. + other: + name: + other: algo mais + desc: + other: Este post requer outro motivo não listado acima. + operation_type: + asked: + other: perguntado + answered: + other: respondido + modified: + other: modificado + deleted_title: + other: Questão excluída + questions_title: + other: Questões + tag: + tags_title: + other: Marcadores + no_description: + other: O marcador não possui descrição. + notification: + action: + update_question: + other: pergunta atualizada + answer_the_question: + other: pergunta respondida + update_answer: + other: resposta atualizada + accept_answer: + other: resposta aceita + comment_question: + other: pergunta comentada + comment_answer: + other: resposta comentada + reply_to_you: + other: respondeu a você + mention_you: + other: mencionou você + your_question_is_closed: + other: A sua pergunta foi fechada + your_question_was_deleted: + other: A sua pergunta foi deletada + your_answer_was_deleted: + other: A sua resposta foi deletada + your_comment_was_deleted: + other: O seu comentário foi deletado + up_voted_question: + other: votos positivos da pergunta + down_voted_question: + other: votos negativos da pergunta + up_voted_answer: + other: votos positivos da resposta + down_voted_answer: + other: votos negativos da resposta + up_voted_comment: + other: votos positivos do comentário + invited_you_to_answer: + other: lhe convidou para responder + earned_badge: + other: Ganhou o emblema "{{.BadgeName}}" emblema + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirme seu novo endereço de e-mail" + body: + other: "Confirme seu novo endereço de e-mail para {{.SiteName}} clicando no seguinte link:
\n{{.ChangeEmailUrl}}

\n\nSe você não solicitou esta alteração, por favor, ignore este email.

\n\n--
\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} respondeu à sua pergunta" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nVisualizar no {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar inscrição" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} convidou-lhe para responder" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
acho que você pode saber a resposta.

\nVisualizar na {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar Inscrição" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} comentou em sua publicação" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nVisualizar no {{.SiteName}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista.

\n\nCancelar inscrição" + new_question: + title: + other: "[{{.SiteName}}] Nova pergunta: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Redefinição de senha" + body: + other: "Alguém pediu para redefinir a sua senha em {{.SiteName}}.

\n\nSe não foi você, você pode ignorar este e-mail.

\n\nClique no seguinte link para escolher uma nova senha:
\n{{.PassResetUrl}}

\n\n--
\nNota: Este é um e-mail de sistema automático, por favor, não responda a esta mensagem, pois a sua resposta não será vista." + register: + title: + other: "[{{.SiteName}}] Confirme seu novo endereço de e-mail" + body: + other: "Bem-vindo a {{.SiteName}}!

\n\nClique no seguinte link para confirmar e ativar sua nova conta:
\n{{.RegisterUrl}}

\n\nSe o link acima não for clicável, tente copiá-lo e colá-lo na barra de endereços do seu navegador web.\n

\n\n--
\nNota: Este é um e-mail de sistema automático. por favor, não responda a esta mensagem, pois a sua resposta não será vista." + test: + title: + other: "[{{.SiteName}}] E-mail de teste" + body: + other: "Este é um e-mail de teste.\n

\n\n--
\nNota: Este é um e-mail automático do sistema, por favor, não responda a esta mensagem, pois a sua resposta não será vista." + action_activity_type: + upvote: + other: voto positivo + upvoted: + other: votos positivos + downvote: + other: voto negativo + downvoted: + other: voto negativo + accept: + other: aceito + accepted: + other: aceito + edit: + other: editar + review: + queued_post: + other: Publicação na fila + flagged_post: + other: Postagem sinalizada + suggested_post_edit: + other: Edições sugeridas + reaction: + tooltip: + other: "{{ .Names }} e mais {{ .Count }}..." + badge: + default_badges: + autobiographer: + name: + other: Autobiógrafo + desc: + other: Preenchido com perfil . + certified: + name: + other: Certificado + desc: + other: Completou o nosso tutorial de novo usuário. + editor: + name: + other: Editor + desc: + other: Primeira edição em publicação. + first_flag: + name: + other: Primeira Sinalização + desc: + other: Primeiro sinalização numa publicação. + first_upvote: + name: + other: Primeiro voto + desc: + other: Primeiro post votado. + first_link: + name: + other: Primeiro link + desc: + other: First added a link to another post. + first_reaction: + name: + other: Primeira Reação + desc: + other: Primeira reação a um post. + first_share: + name: + other: Primeiro Compartilhamento + desc: + other: Primeiro a compartilhar um post. + scholar: + name: + other: Académico + desc: + other: Fez uma pergunta e aceitou uma resposta. + commentator: + name: + other: Comentador + desc: + other: Fez 5 comentários. + new_user_of_the_month: + name: + other: Novo usuário do mês + desc: + other: Contribuições pendentes no seu primeiro mês. + read_guidelines: + name: + other: Ler diretrizes + desc: + other: Leia as [diretrizes da comunidade]. + reader: + name: + other: Leitor + desc: + other: Leia todas as respostas num tópico com mais de 10 respostas. + welcome: + name: + other: Bem-vindo + desc: + other: Recebeu um voto positivo. + nice_share: + name: + other: Bom compartilhador + desc: + other: Compartilhou um post com 25 visitantes únicos. + good_share: + name: + other: Bom compartilhador + desc: + other: Compartilhou um post com 300 visitantes únicos. + great_share: + name: + other: Grande Compartilhador + desc: + other: Compartilhou um post com 1000 visitantes únicos. + out_of_love: + name: + other: Por amor + desc: + other: Cinquenta votos positivos em um dia. + higher_love: + name: + other: Amor Superior + desc: + other: Usou 50 votos positivos em um dia — 5 vezes. + crazy_in_love: + name: + other: Amor Louco + desc: + other: Usou 50 votos positivos em um dia — 20 vezes. + promoter: + name: + other: Promotor + desc: + other: Convidou um usuário. + campaigner: + name: + other: Ativista + desc: + other: Foram convidados 3 usuários básicos. + champion: + name: + other: Campeão + desc: + other: Cinco membros convidados. + thank_you: + name: + other: Obrigado + desc: + other: Recebeu 20 votos positivos e deu 10 votos. + gives_back: + name: + other: Dar de volta + desc: + other: Recebeu100 votos positivos e deu 100 votos a favor. + empathetic: + name: + other: Empático + desc: + other: Recebeu 500 votos e deu 1000 votos. + enthusiast: + name: + other: Entusiasta + desc: + other: . + aficionado: + name: + other: Aficionado + desc: + other: Visitou 100 dias consecutivos. + devotee: + name: + other: Devoto + desc: + other: Visitou 365 dias consecutivos. + anniversary: + name: + other: Aniversário + desc: + other: Membro ativo por um ano, postando pelo menos uma vez. + appreciated: + name: + other: Apreciado + desc: + other: Recebeu 1 voto positivo em 20 posts. + respected: + name: + other: Respeitado + desc: + other: Novos 2 votos em 100 posts. + admired: + name: + other: Admirado + desc: + other: Recebeu 5 votos positivos em 300 posts. + solved: + name: + other: Resolvido + desc: + other: Ter uma resposta aceita. + guidance_counsellor: + name: + other: Orientador Educacional + desc: + other: Tenham 10 respostas aceitas. + know_it_all: + name: + other: Sabichão + desc: + other: Tenha 50 respostas aceitas. + solution_institution: + name: + other: Instituição de Soluções + desc: + other: Tenham 150 respostas aceitas. + nice_answer: + name: + other: Resposta legal + desc: + other: Resposta com pontuação maior que 10. + good_answer: + name: + other: Boa resposta + desc: + other: Resposta com pontuação maior que 25. + great_answer: + name: + other: Ótima Resposta + desc: + other: Resposta com pontuação maior que 50. + nice_question: + name: + other: Questão legal + desc: + other: Pergunta com pontuação maior que 10. + good_question: + name: + other: Boa questão + desc: + other: Questão com pontuação maior que 25. + great_question: + name: + other: Otima questão + desc: + other: Questão com pontuação maior que 50. + popular_question: + name: + other: Pergunta Popular + desc: + other: Pergunta com 500 visualizações. + notable_question: + name: + other: Pergunta Notável + desc: + other: Pergunta com 1000 visualizações. + famous_question: + name: + other: Pergunta Famosa + desc: + other: Pergunta com 5000 visualizações. + popular_link: + name: + other: Link Popular + desc: + other: Postou um link externo com 50 cliques. + hot_link: + name: + other: Link Quente + desc: + other: Postou um link externo com 300 cliques. + famous_link: + name: + other: Link Famoso + desc: + other: Postou um link externo com 100 cliques. + default_badge_groups: + getting_started: + name: + other: Começando + community: + name: + other: Comunidade + posting: + name: + other: Postando +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Como formatar + desc: >- + + pagination: + prev: Anterior + next: Próximo + page_title: + question: Pergunta + questions: Perguntas + tag: Marcador + tags: Marcadores + tag_wiki: marcador wiki + create_tag: Criar Marcador + edit_tag: Editar Marcador + ask_a_question: Create Question + edit_question: Editar Pergunta + edit_answer: Editar Resposta + search: Busca + posts_containing: Postagens contendo + settings: Configurações + notifications: Notificações + login: Entrar + sign_up: Registar-se + account_recovery: Recuperação de conta + account_activation: Ativação de conta + confirm_email: Confirmar E-mail + account_suspended: Conta suspensa + admin: Administrador + change_email: Modificar e-mail + install: Instalação do Answer + upgrade: Atualização do Answer + maintenance: Manutenção do website + users: Usuários + oauth_callback: Processando + http_404: HTTP Erro 404 + http_50X: HTTP Erro 500 + http_403: HTTP Erro 403 + logout: Encerrar Sessão + notifications: + title: Notificações + inbox: Caixa de entrada + achievement: Conquistas + new_alerts: Novos alertas + all_read: Marcar todos como lida + show_more: Mostrar mais + someone: Alguém + inbox_type: + all: Tudo + posts: Postagens + invites: Convites + votes: Votos + answer: Resposta + question: Questão + badge_award: Emblema + suspended: + title: A sua conta foi suspensa + until_time: "Sua conta está suspensa até {{ time }}." + forever: Este usuário foi suspenso permanentemente. + end: Você não atende a uma diretriz da comunidade. + contact_us: Entre em contato conosco + editor: + blockquote: + text: Bloco de citação + bold: + text: Negrito + chart: + text: Gráfico + flow_chart: Gráfico de fluxo + sequence_diagram: Diagrama de sequência + class_diagram: Diagrama de classe + state_diagram: Diagrama de estado + entity_relationship_diagram: Diagrama de relacionamento de entidade + user_defined_diagram: Diagrama definido pelo usuário + gantt_chart: Gráfico de Gantt + pie_chart: Gráfico de pizza + code: + text: Exemplo de código + add_code: Adicionar exemplo de código + form: + fields: + code: + label: Código + msg: + empty: Código não pode ser vazio. + language: + label: Idioma + placeholder: Deteção automática + btn_cancel: Cancelar + btn_confirm: Adicionar + formula: + text: Fórmula + options: + inline: Fórmula na linha + block: Bloco de fórmula + heading: + text: Cabeçalho + options: + h1: Cabeçalho 1 + h2: Cabeçalho 2 + h3: Cabeçalho 3 + h4: Cabeçalho 4 + h5: Cabeçalho 5 + h6: Cabeçalho 6 + help: + text: Ajuda + hr: + text: Régua horizontal + image: + text: Imagem + add_image: Adicionar imagem + tab_image: Enviar image, + form_image: + fields: + file: + label: Arquivo de imagem + btn: Selecione imagem + msg: + empty: Arquivo não pode ser vazio. + only_image: Somente um arquivo de imagem é permitido. + max_size: O tamanho do arquivo não pode exceder {{size}} MB. + desc: + label: Descrição (opcional) + tab_url: URL da imagem + form_url: + fields: + url: + label: URL da imagem + msg: + empty: URL da imagem não pode ser vazia. + name: + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar + uploading: Enviando + indent: + text: Identação + outdent: + text: Não identado + italic: + text: Emphase + link: + text: Superlink (Hyperlink) + add_link: Adicionar superlink (hyperlink) + form: + fields: + url: + label: URL + msg: + empty: URL não pode ser vazia. + name: + label: Descrição (opcional) + btn_cancel: Cancelar + btn_confirm: Adicionar + ordered_list: + text: Lista numerada + unordered_list: + text: Lista com marcadores + table: + text: Tabela + heading: heading + cell: Célula + file: + text: Anexar arquivos + not_supported: "Não suporta esse tipo de arquivo. Tente novamente com {{file_type}}." + max_size: "Anexar arquivos não pode exceder {{size}} MB." + close_modal: + title: Estou fechando este post como... + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: Não pode ser vazio. + msg: + empty: Por favor selecione um motivo. + report_modal: + flag_title: Estou marcando para denunciar este post como... + close_title: Estou fechando este post como... + review_question_title: Revisar pergunta + review_answer_title: Revisar resposta + review_comment_title: Revisar comentário + btn_cancel: Cancelar + btn_submit: Enviar + remark: + empty: Não pode ser vazio. + msg: + empty: Por favor selecione um motivo. + not_a_url: Formato da URL incorreto. + url_not_match: A origem da URL não corresponde ao site atual. + tag_modal: + title: Criar novo marcador + form: + fields: + display_name: + label: Nome de exibição + msg: + empty: Nome de exibição não pode ser vazio. + range: Nome de exibição tem que ter até 35 caracteres. + slug_name: + label: Slug de URL + desc: 'Deve usar o conjunto de caracteres "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug não pode ser vazio. + range: URL slug até 35 caracteres. + character: URL slug contém conjunto de caracteres não permitido. + desc: + label: Descrição (opcional) + revision: + label: Revisão + edit_summary: + label: Editar descrição + placeholder: >- + Explique resumidamente as suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_cancel: Cancelar + btn_submit: Enviar + btn_post: Postar novo marcador + tag_info: + created_at: Criado + edited_at: Editado + history: Histórico + synonyms: + title: Sinônimos + text: Os marcadores a seguir serão re-mapeados para + empty: Sinônimos não encotrados. + btn_add: Adicionar um sinônimo + btn_edit: Editar + btn_save: Salvar + synonyms_text: Os marcadores a seguir serão re-mapeados para + delete: + title: Remover este marcador + tip_with_posts: >- +

Nós não permitimos remover marcadores com postagens.

Por favor, remova este marcador a partir da postagem.

+ tip_with_synonyms: >- +

Nós não permitimos remover marcadores com postagens.

Por favor, remova este marcador a partir da postagem.

+ tip: Você tem certeza que deseja remover? + close: Fechar + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Editar marcador + default_reason: Editar marcador + default_first_reason: Adicionar marcador + btn_save_edits: Salvar edições + btn_cancel: Cancelar + dates: + long_date: D MMM + long_date_with_year: "D MMM, YYYY" + long_date_with_time: "D MMM, YYYY [at] HH:mm" + now: agora + x_seconds_ago: "{{count}}s atrás" + x_minutes_ago: "{{count}}m atrás" + x_hours_ago: "{{count}}h atrás" + hour: hora + day: dia + hours: horas + days: dias + month: month + months: months + year: year + reaction: + heart: coração + smile: sorrir + frown: cara feia + btn_label: adicionar ou remover reações + undo_emoji: desfazer reação {{ emoji }} + react_emoji: reagir com {{ emoji }} + unreact_emoji: remover reação {{ emoji }} + comment: + btn_add_comment: Adicionar comentário + reply_to: Responder a + btn_reply: Responder + btn_edit: Editar + btn_delete: Excluir + btn_flag: Marcador + btn_save_edits: Salvar edições + btn_cancel: Cancelar + show_more: "Mais {{count}} comentários" + tip_question: >- + Use os comentários para pedir mais informações ou sugerir melhorias. Evite responder perguntas nos comentários. + tip_answer: >- + Use comentários para responder a outros usuários ou notificá-los sobre alterações. Se você estiver adicionando novas informações, edite sua postagem em vez de comentar. + tip_vote: Isto adiciona alguma utilidade à postagem + edit_answer: + title: Editar Resposta + default_reason: Editar Resposta + default_first_reason: Adicionar resposta + form: + fields: + revision: + label: Revisão + answer: + label: Resposta + feedback: + characters: conteúdo deve ser pelo menos 6 characters em comprimento. + edit_summary: + label: Resumo da edição + placeholder: >- + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_save_edits: Salvar edições + btn_cancel: Cancelar + tags: + title: Marcadores + sort_buttons: + popular: Popular + name: Nome + newest: mais recente + button_follow: Seguir + button_following: Seguindo + tag_label: perguntas + search_placeholder: Filtrar por nome de marcador + no_desc: O marcador não possui descrição. + more: Mais + wiki: Wiki + ask: + title: Create Question + edit_title: Editar Pergunta + default_reason: Editar pergunta + default_first_reason: Create question + similar_questions: Similar perguntas + form: + fields: + revision: + label: Revisão + title: + label: Título + placeholder: What's your topic? Be specific. + msg: + empty: Título não pode ser vazio. + range: Título até 150 caracteres + body: + label: Corpo + msg: + empty: Corpo da mensagem não pode ser vazio. + tags: + label: Marcadores + msg: + empty: Marcadores não podes ser vazios. + answer: + label: Resposta + msg: + empty: Resposta não pode ser vazia. + edit_summary: + label: Resumo da edição + placeholder: >- + Explique resumidamente suas alterações (ortografia corrigida, gramática corrigida, formatação aprimorada) + btn_post_question: Publicação a sua pergunta + btn_save_edits: Salvar edições + answer_question: Responda a sua própria pergunta + post_question&answer: Publicação a sua pergunta e resposta + tag_selector: + add_btn: Adicionar marcador + create_btn: Criar novo marcador + search_tag: Procurar marcador + hint: "Describe what your content is about, at least one tag is required." + no_result: Nenhum marcador correspondente + tag_required_text: Marcador obrigatório (ao menos um) + header: + nav: + question: Perguntas + tag: Marcadores + user: Usuários + badges: Emblemas + profile: Perfil + setting: Configurações + logout: Sair + admin: Administrador + review: Revisar + bookmark: Favoritos + moderation: Moderação + search: + placeholder: Procurar + footer: + build_on: >- + Construído com <1> Apache answer o software de código aberto que ajuda comunidades de Q&A.
Feito com amor © {{cc}}. + upload_img: + name: Mudar + loading: carregando... + pic_auth_code: + title: Captcha + placeholder: Escreva o texto acima + msg: + empty: Captcha não pode ser vazio. + inactive: + first: >- + Você está quase pronto! Enviamos um e-mail de ativação para {{mail}}. Por favor, siga as instruções no e-mail para ativar uma conta sua. + info: "Se não chegar, verifique sua pasta de spam." + another: >- + Enviamos outro e-mail de ativação para você em {{mail}}. Pode levar alguns minutos para chegar; certifique-se de verificar sua pasta de spam. + btn_name: Reenviar e-mail de ativação + change_btn_name: Mudar email + msg: + empty: Não pode ser vazio. + resend_email: + url_label: Tem certeza de que deseja reenviar o e-mail de ativação? + url_text: Você também pode fornecer o link de ativação acima para o usuário. + login: + login_to_continue: Entre para continue + info_sign: Não possui uma conta? <1>Cadastrar-se + info_login: Já possui uma conta? <1>Entre + agreements: Ao se registrar, você concorda com as <1>políticas de privacidades e os <3>termos de serviços. + forgot_pass: Esqueceu a sua senha? + name: + label: Nome + msg: + empty: Nome não pode ser vazio. + range: O nome deve ter entre 2 e 30 caracteres. + character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"' + email: + label: E-mail + msg: + empty: E-mail não pode ser vazio. + password: + label: Senha + msg: + empty: Senha não pode ser vazia. + different: As senhas inseridas em ambos os campos são inconsistentes + account_forgot: + page_title: Esqueceu a sua senha + btn_name: Enviar e-mail de recuperação de senha + send_success: >- + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. + email: + label: E-mail + msg: + empty: E-mail não pode ser vazio. + change_email: + btn_cancel: Cancelar + btn_update: Atualiza email address + send_success: >- + Se uma conta corresponder {{mail}}, você deve receber um e-mail com instruções sobre como redefinir sua senha em breve. + email: + label: Novo E-mail + msg: + empty: E-mail não pode ser vazio. + oauth: + connect: Conecte com {{ auth_name }} + remove: Remover {{ auth_name }} + oauth_bind_email: + subtitle: Adicione um e-mail para recuperação da conta. + btn_update: Atualizar endereço de e-mail + email: + label: E-mail + msg: + empty: E-mail não pode ser vazio. + modal_title: E-mail já existe. + modal_content: Este e-mail já está cadastrado. Você tem certeza que deseja conectar à conta existente? + modal_cancel: Alterar E-mail + modal_confirm: Conectar a uma conta existente + password_reset: + page_title: Redefinir senha + btn_name: Redefinir minha senha + reset_success: >- + Você alterou com sucesso uma senha sua; você será redirecionado para a página de login. + link_invalid: >- + Desculpe, este link de redefinição de senha não é mais válido. Talvez uma senha sua já tenha sido redefinida? + to_login: Continuar para a tela de login + password: + label: Senha + msg: + empty: Senha não pode ser vazio. + length: O comprimento deve estar entre 8 e 32 + different: As senhas inseridas em ambos os campos são inconsistentes + password_confirm: + label: Confirmar Nova senha + settings: + page_title: Configurações + goto_modify: Ir para modificar + nav: + profile: Perfil + notification: Notificações + account: Conta + interface: Interface + profile: + heading: Perfil + btn_name: Salvar + display_name: + label: Nome de exibição + msg: Nome de exibição não pode ser vazio. + msg_range: Display name must be 2-30 characters in length. + username: + label: Nome de usuário + caption: As pessoas poderão mensionar você com "@usuário". + msg: Nome de usuário não pode ser vazio. + msg_range: Username must be 2-30 characters in length. + character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"' + avatar: + label: Perfil Imagem + gravatar: Gravatar + gravatar_text: Você pode mudar a imagem em <1>gravatar.com + custom: Customizado + custom_text: Você pode enviar a sua image. + default: Padrão do Sistema + msg: Por favor envie um avatar + bio: + label: Sobre mim (opcional) + website: + label: Website (opcional) + placeholder: "https://exemplo.com.br" + msg: Formato incorreto de endereço de Website + location: + label: Localização (opcional) + placeholder: "Cidade, País" + notification: + heading: Notificações por e-mail + turn_on: Ativar + inbox: + label: Notificações na caixa de entrada + description: Responda suas próprias perguntas, comentários, convites e muito mais. + all_new_question: + label: Todas as perguntas novas + description: Seja notificado de todas as novas perguntas. Até 50 perguntas por semana. + all_new_question_for_following_tags: + label: Novas perguntas para os seguintes marcadores + description: Seja notificado de novas perguntas para os seguintes marcadores. + account: + heading: Conta + change_email_btn: Mudar e-mail + change_pass_btn: Mudar senha + change_email_info: >- + Enviamos um e-mail para esse endereço. Siga as instruções de confirmação. + email: + label: Correio eletrónico + new_email: + label: Novo correio eletrónico + msg: Novo correio eletrónico não pode ser vazio. + pass: + label: Senha atual + msg: Senha não pode ser vazio. + password_title: Senha + current_pass: + label: Senha atual + msg: + empty: A Senha não pode ser vazia. + length: O comprimento deve estar entre 8 and 32. + different: As duas senhas inseridas não correspondem. + new_pass: + label: Nova Senha + pass_confirm: + label: Confirmar nova Senha + interface: + heading: Interface + lang: + label: Idioma da Interface + text: Idioma da interface do usuário. A interface mudará quando você atualizar a página. + my_logins: + title: Meus logins + label: Entre ou cadastre-se neste site utilizando estas contas. + modal_title: Remover login + modal_content: Você tem certeza que deseja remover este login da sua conta? + modal_confirm_btn: Remover + remove_success: Removido com sucesso + toast: + update: atualização realizada com sucesso + update_password: Senha alterada com sucesso. + flag_success: Obrigado por marcar. + forbidden_operate_self: Proibido para operar por você mesmo + review: A sua resposta irá aparecer após a revisão. + sent_success: Enviado com sucesso + related_question: + title: Related + answers: respostas + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Pessoas Perguntaram + desc: Select people who you think might know the answer. + invite: Convidar para responder + add: Adicionar pessoas + search: Procurar pessoas + question_detail: + action: Acção + Asked: Perguntado + asked: perguntado + update: Modificado + edit: modificado + commented: comentado + Views: Visualizado + Follow: Seguir + Following: Seguindo + follow_tip: Siga esta pergunta para receber notificações + answered: respondido + closed_in: Fechado em + show_exist: Mostrar pergunta existente. + useful: Útil + question_useful: Isso é útil e claro + question_un_useful: Isso não está claro ou não é útil + question_bookmark: Favoritar esta pergunta + answer_useful: Isso é útil + answer_un_useful: Isso não é útil + answers: + title: Respostas + score: Pontuação + newest: Mais recente + oldest: Mais Antigos + btn_accept: Aceito + btn_accepted: Aceito + write_answer: + title: A sua Resposta + edit_answer: Editar a minha resposta existente + btn_name: Publicação a sua resposta + add_another_answer: Adicionar outra resposta + confirm_title: Continuar a responder + continue: Continuar + confirm_info: >- +

Tem certeza de que deseja adicionar outra resposta?

Você pode usar o link de edição para refinar e melhorar uma resposta existente.

+ empty: Resposta não pode ser vazio. + characters: conteúdo deve ser pelo menos 6 caracteres em comprimento. + tips: + header_1: Obrigado pela sua resposta + li1_1: Por favor, não esqueça de responder a pergunta. Providencie detalhes e compartilhe a sua pesquisa. + li1_2: Faça backup de quaisquer declarações que você fizer com referências ou experiência pessoal. + header_2: Mas evite ... + li2_1: Pedir ajuda, buscar esclarecimentos ou responder a outras respostas. + reopen: + confirm_btn: Reabrir + title: Reabrir esta postagem + content: Você tem certeza que deseja reabrir? + list: + confirm_btn: Lista + title: Liste esta postagem + content: Você tem certeza que deseja listar? + unlist: + confirm_btn: Remover da lista + title: Remover da lista de postagens + content: Tem certeza de que deseja remover da lista? + pin: + title: Fixe esta postagem + content: Tem certeza de que deseja fixar globalmente? Esta postagem aparecerá no topo de todas as listas de postagens. + confirm_btn: Fixar + delete: + title: Excluir esta postagem + question: >- + Nós não recomendamos excluindo perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

Repeated deletion of answered questions can result in a sua account being blocked from asking. Você tem certeza que deseja deletar? + answer_accepted: >- +

Nós não recomendamos excluir perguntas com respostas porque isso priva os futuros leitores desse conhecimento.

A exclusão repetida de respostas aceitas pode resultar no bloqueio de respostas de sua conta.. Você tem certeza que deseja deletar? + other: Você tem certeza que deseja deletar? + tip_answer_deleted: Esta resposta foi deletada + undelete_title: Recuperar esta publicação + undelete_desc: Você tem certeza que deseja recuperar? + btns: + confirm: Confirmar + cancel: Cancelar + edit: Editar + save: Salvar + delete: Excluir + undelete: Recuperar + list: Lista + unlist: Não listar + unlisted: Não listado + login: Entrar + signup: Cadastrar-se + logout: Sair + verify: Verificar + create: Create + approve: Aprovar + reject: Rejetar + skip: Pular + discard_draft: Descartar rascunho + pinned: Fixado + all: Todos + question: Pergunta + answer: Resposta + comment: Comentário + refresh: Atualizar + resend: Reenviar + deactivate: Desativar + active: Ativar + suspend: Suspender + unsuspend: Suspensão cancelada + close: Fechar + reopen: Reabrir + ok: OK + light: Claro + dark: Escuro + system_setting: Definições de sistema + default: Padrão + reset: Reset + tag: Marcador + post_lowercase: publicação + filter: Filtro + ignore: Ignorar + submit: Submeter + normal: Normal + closed: Fechado + deleted: Removido + deleted_permanently: Deleted permanently + pending: Pendente + more: Mais + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Procurar Resultados + keywords: Palavras-chave + options: Opções + follow: Seguir + following: Seguindo + counts: "{{count}} Resultados" + counts_loading: "... Results" + more: Mais + sort_btns: + relevance: Relevância + newest: Mais recente + active: Ativar + score: Pontuação + more: Mais + tips: + title: Dicas de Pesquisa Avançada + tag: "<1>[tag] pesquisar com um marcador" + user: "<1>user:username buscar por autor" + answer: "<1>answers:0 perguntas não respondidas" + score: "<1>score:3 postagens com mais de 3+ placares" + question: "<1>is:question buscar perguntas" + is_answer: "<1>is:answer buscar respostas" + empty: Não conseguimos encontrar nada.
Tente palavras-chave diferentes ou menos específicas. + share: + name: Compartilhar + copy: Copiar link + via: Compartilhar postagem via... + copied: Copiado + facebook: Compartilhar no Facebook + twitter: Share to X + cannot_vote_for_self: Você não pode votar na sua própria postagem + modal_confirm: + title: Erro... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: A sua nova conta está confirmada; você será redirecionado para a página inicial. + link: Continuar para a página inicial. + oops: Oops! + invalid: O link utilizado não funciona mais. + confirm_new_email: O seu e-mail foi atualizado. + confirm_new_email_invalid: >- + Desculpe, este link de confirmação não é mais válido. Talvez o seu e-mail já tenha sido alterado. + unsubscribe: + page_title: Cancelar subscrição + success_title: Cancelamento de inscrição bem-sucedido + success_desc: Você foi removido com sucesso desta lista de assinantes e não receberá mais nenhum e-mail nosso. + link: Mudar configurações + question: + following_tags: Seguindo Marcadores + edit: Editar + save: Salvar + follow_tag_tip: Seguir tags to curate a sua lista de perguntas. + hot_questions: Perguntas quentes + all_questions: Todas Perguntas + x_questions: "{{ count }} perguntas" + x_answers: "{{ count }} respostas" + x_posts: "{{ count }} Posts" + questions: Perguntas + answers: Respostas + newest: Mais recente + active: Ativo + hot: Popular + frequent: Frequente + recommend: Recomendado + score: Pontuação + unanswered: Não Respondido + modified: modificado + answered: respondido + asked: perguntado + closed: fechado + follow_a_tag: Seguir o marcador + more: Mais + personal: + overview: Visão geral + answers: Respostas + answer: resposta + questions: Perguntas + question: pergunta + bookmarks: Favoritas + reputation: Reputação + comments: Comentários + votes: Votos + badges: Emblemas + newest: Mais recente + score: Pontuação + edit_profile: Editar Perfil + visited_x_days: "Visitado {{ count }} dias" + viewed: Visualizado + joined: Ingressou + comma: "," + last_login: Visto + about_me: Sobre mim + about_me_empty: "// Olá, Mundo !" + top_answers: Melhores Respostas + top_questions: Melhores Perguntas + stats: Estatísticas + list_empty: Postagens não encontradas.
Talvez você queira selecionar uma guia diferente? + content_empty: Nenhum post encontrado. + accepted: Aceito + answered: respondido + asked: perguntado + downvoted: voto negativo + mod_short: Moderador + mod_long: Moderadores + x_reputation: reputação + x_votes: votos recebidos + x_answers: respostas + x_questions: perguntas + recent_badges: Emblemas recentes + install: + title: Instalação + next: Proximo + done: Completo + config_yaml_error: Não é possível criar o arquivo config.yaml. + lang: + label: Por favor Escolha um Idioma + db_type: + label: Motor do Banco de dados + db_username: + label: Nome de usuário + placeholder: raiz + msg: Nome de usuário não pode ser vazio. + db_password: + label: Senha + placeholder: raiz + msg: Senha não pode ser vazio. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host não pode ser vazio. + db_name: + label: Database Nome + placeholder: resposta + msg: Database Nome não pode ser vazio. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File não pode ser vazio. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Criar config.yaml + label: Arquivo config.yaml criado. + desc: >- + Você pode criar o arquivo <1>config.yaml manualmente no diretório <1>/var/ww/xxx/ e colar o seguinte texto nele. + info: Qlique no botão "Próximo" após finalizar. + site_information: Informação do Site + admin_account: Administrador Conta + site_name: + label: Site Nome + msg: Site Nome não pode ser vazio. + msg_max_length: O nome do site deve ter no máximo 30 caracteres. + site_url: + label: URL do Site + text: O endereço do seu site. + msg: + empty: Site URL não pode ser vazio. + incorrect: URL do site está incorreto. + max_length: O nome do site deve ter no máximo 512 caracteres. + contact_email: + label: E-mail par contato + text: O endereço de e-mail do contato principal deste site. + msg: + empty: E-mail par contato não pode ser vazio. + incorrect: E-mail par contato incorrect format. + login_required: + label: Privado + switch: É necessário fazer login + text: Somente usuários conectados podem acessar esta comunidade. + admin_name: + label: Nome + msg: Nome não pode ser vazio. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Senha + text: >- + You will need this password to log in. Por favor store it in a secure location. + msg: Senha não pode ser vazio. + msg_min_length: A senha deve ser ter pelo menos 8 caracteres. + msg_max_length: A senha deve ter no máximo 32 caracteres. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: E-mail + text: Você precisará deste e-mail para efetuar o login. + msg: + empty: Email não pode ser vazio. + incorrect: O formato do e-mail está incorreto. + ready_title: Seu site está pronto + ready_desc: >- + Se você quiser alterar mais configurações, visite <1>seção de administrador; encontre-o no menu do site. + good_luck: "Divirta-se, e boa sorte!" + warn_title: Atenção + warn_desc: >- + O arquivo <1>config.yaml já existe. Se você precisa redefinir algum dos itens de configuração deste arquivo, apague-o primeiro. + install_now: Você pode tentar <1>instalando agora. + installed: Já instalado + installed_desc: >- + You appear to have already installed. To reinstall please clear a sua old database tables first. + db_failed: Falha ao conectar-se ao banco de dados + db_failed_desc: >- + This either means that the database information in a sua <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean a sua host's database server is down. + counts: + views: visualizações + votes: votos + answers: respostas + accepted: Aceito + page_error: + http_error: Erro HTTP {{ code }} + desc_403: Você não possui permissão para acessar esta página. + desc_404: Infelizmente esta página não existe. + desc_50X: Houve um erro no servidor e não foi possível completar a sua requisição. + back_home: Voltar para a página inicial + page_maintenance: + desc: "Estamos em manutenção, voltaremos em breve." + nav_menus: + dashboard: Painel + contents: Conteúdos + questions: Perguntas + answers: Respostas + users: Usuários + badges: Emblemas + flags: Marcadores + settings: Configurações + general: Geral + interface: Interface + smtp: SMTP + branding: Marca + legal: Informação legal + write: Escrever + tos: Termos de Serviços + privacy: Privacidade + seo: SEO + customize: Personalização + themes: Temas + login: Entrar + privileges: Privilégios + plugins: Extensões + installed_plugins: Plugins instalados + apperance: Appearance + website_welcome: Bem vindo(a) ao {{site_name}} + user_center: + login: Entrar + qrcode_login_tip: Por favor, utilize {{ agentName }} para escanear o QR code para entrar. + login_failed_email_tip: Falha ao entrar, por favor, permita que este aplicativo acesse a informação do seu e-mail antes de tentar novamente. + badges: + modal: + title: Parabéns + content: Você ganhou um novo emblema. + close: Fechar + confirm: Ver emblemas + title: Emblemas + awarded: Premiado + earned_×: Ganhou ×{{ number }} + ×_awarded: "{{ number }} premiado" + can_earn_multiple: Você pode ganhar isto várias vezes. + earned: Ganhou + admin: + admin_header: + title: Administrador + dashboard: + title: Painel + welcome: Bem-vindo ao Admin! + site_statistics: Estatísticas do site + questions: "Perguntas:" + resolved: "Resolvido:" + unanswered: "Não Respondido:" + answers: "Respostas:" + comments: "Comentários:" + votes: "Votos:" + users: "Usuários:" + flags: "Marcadores:" + reviews: "Revisão:" + site_health: Saúde do site + version: "Versão:" + https: "HTTPS:" + upload_folder: "Upload da pasta:" + run_mode: "Mode de execução:" + private: Privado + public: Público + smtp: "SMTP:" + timezone: "Fuso horário:" + system_info: Informação do sistema + go_version: "Versão do Go:" + database: "Banco de dados:" + database_size: "Tamanho do banco de dados:" + storage_used: "Armazenamento usado:" + uptime: "Tempo de atividade:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contato + forum: Fórum + documents: Documentos + feedback: Opinião + support: Supporte + review: Revisar + config: Configurações + update_to: Atualizar ao + latest: Ultimo + check_failed: Falha na verificação + "yes": "Sim" + "no": "Não" + not_allowed: Não permitido + allowed: Permitido + enabled: Ativo + disabled: Disponível + writable: Possível escrever + not_writable: Não é possível escrever + flags: + title: Marcadores + pending: Pendente + completed: Completo + flagged: Marcado + flagged_type: '{{ type }} sinalizado' + created: Criado + action: Ação + review: Revisar + user_role_modal: + title: Altere a função do usuário para... + btn_cancel: Cancelar + btn_submit: Enviar + new_password_modal: + title: Criar nova senha + form: + fields: + password: + label: Senha + text: O usuário será desconectado e precisar entrar novamente. + msg: A senha precisa ter no mínimo 8-32 caracteres. + btn_cancel: Cancelar + btn_submit: Enviar + edit_profile_modal: + title: Editar profile + form: + fields: + display_name: + label: Nome no display + msg_range: Display name must be 2-30 characters in length. + username: + label: Nome do usuário + msg_range: Username must be 2-30 characters in length. + email: + label: Correio eletrônico + msg_invalid: Correio eletrônico invalido. + edit_success: Editado com sucesso + btn_cancel: Cancelar + btn_submit: Submeter + user_modal: + title: Adicionar novo usuário + form: + fields: + users: + label: Adicionar usuários em massa + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separe "nome, e-mail, senha" com vírgulas. Um usuário por linha. + msg: "Por favor insira o e-mail do usuário, um por linha." + display_name: + label: Nome de exibição + msg: O nome de exibição deve ter entre 2 e 30 caracteres. + email: + label: E-mail + msg: E-mail inválido. + password: + label: Senha + msg: A senha precisa ter no mínimo 8-32 caracteres. + btn_cancel: Cancelar + btn_submit: Enviar + users: + title: Usuários + name: Nome + email: E-mail + reputation: Reputação + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Estado + role: Função + action: Ação + change: Mudar + all: Todos + staff: Funcionários + more: Mais + inactive: Inativo + suspended: Suspenso + deleted: Removido + normal: Normal + Moderator: Moderador + Admin: Administrador + User: Usuário + filter: + placeholder: "Filtrar por nome, user:id" + set_new_password: Configurar nova senha + edit_profile: Editar profile + change_status: Mudar status + change_role: Mudar função + show_logs: Mostrar registros + add_user: Adicionar usuário + deactivate_user: + title: Desativar usuários + content: Um usuário inativo deve revalidar seu e-mail. + delete_user: + title: Remover este usuário + content: Tem certeza de que deseja excluir este usuário? Isso é permanente! + remove: Remover o conteúdo dele + label: Remover todas as perguntas, respostas, comentários etc. + text: Não marque isso se deseja excluir apenas a conta do usuário. + suspend_user: + title: Suspender este usuário + content: Um usuário suspenso não pode fazer login. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Perguntas + unlisted: Não-listado + post: Publicação + votes: Votos + answers: Respostas + created: Criado + status: Estado + action: Ação + change: Mudar + pending: Pendente + filter: + placeholder: "Filtrar por título, question:id" + answers: + page_title: Respostas + post: Publicação + votes: Votos + created: Criado + status: Estado + action: Ação + change: Mudar + filter: + placeholder: "Filtrar por título, answer:id" + general: + page_title: Geral + name: + label: Site Nome + msg: Site name não pode ser vazio. + text: "O nome deste site, conforme usado na tag de título." + site_url: + label: URL do Site + msg: Site url não pode ser vazio. + validate: Por favor digite uma URL válida. + text: O endereço do seu site. + short_desc: + label: Breve Descrição do site (opcional) + msg: Breve Descrição do site não pode ser vazio. + text: "Breve descrição, conforme usado na tag de título na página inicial." + desc: + label: Site Descrição (opcional) + msg: Descrição do site não pode ser vazio. + text: "Descreva este site em uma única sentença, conforme usado na meta tag de descrição." + contact_email: + label: E-mail para contato + msg: E-mail par contato não pode ser vazio. + validate: E-mail par contato não é válido. + text: Endereço de e-mail do principal contato responsável por este site. + check_update: + label: Atualizações de software + text: Verificar se há atualizações automaticamente + interface: + page_title: Interface + language: + label: Idioma da interface + msg: Idioma da Interface não pode ser vazio. + text: Idioma da interface do Usuário. Ele mudará quando você atualizar a página. + time_zone: + label: Fuso horário + msg: Fuso horário não pode ser vazio. + text: Escolha a cidade no mesmo fuso horário que você. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: E-mail de origem + msg: E-mail de origem não pode ser vazio. + text: O endereço de e-mail de onde os e-mails são enviados. + from_name: + label: Nome de origem + msg: Nome de origem não pode ser vazio. + text: O nome de onde os e-mails são enviados. + smtp_host: + label: SMTP Host + msg: SMTP host não pode ser vazio. + text: O seu servidor de e-mails. + encryption: + label: Criptografia + msg: Criptografia não pode ser vazio. + text: Para a maioria dos servidores SSL é a opção recomendada. + ssl: SSL + tls: TLS + none: Nenhum + smtp_port: + label: SMTP Port + msg: Porta SMTP deve ser o número 1 ~ 65535. + text: The port to a sua mail server. + smtp_username: + label: SMTP Nome de usuário + msg: SMTP username não pode ser vazio. + smtp_password: + label: SMTP Senha + msg: SMTP password não pode ser vazio. + test_email_recipient: + label: Test Email Recipients + text: Forneça o endereço de e-mail que irá receber envios de teste. + msg: O e-mail de teste é inválido + smtp_authentication: + label: Habilitar autenticação + title: Autenticação SMTP + msg: Autenticação SMTP não pode ser vazio. + "yes": "Sim" + "no": "Não" + branding: + page_title: Marca + logo: + label: Logo (opcional) + msg: Logo não pode ser vazio. + text: The logo image at the top left of a sua site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (opcional) + text: The logo used on mobile version of a sua site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (opcional) + msg: Square icon não pode ser vazio. + text: Imagem used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (opcional) + text: A favicon for a sua site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Informação legal + terms_of_service: + label: Terms of Service + text: "Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui." + privacy_policy: + label: Privacy Policy + text: "Você pode adicionar termos de conteúdo de serviço aqui. Se você já tem um documento hospedado em outro lugar, forneça a URL completa aqui." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Escrever + restrict_answer: + title: Escrever resposta + label: Each user can only write one answer for each question + text: "Desative para permitir que os usuários escrevam várias respostas para a mesma pergunta, o que pode fazer com que as respostas fiquem menos focadas." + recommend_tags: + label: Recommend Marcadores + text: "Os marcadores recomendados serão exibidos na lista dropdown por padrão." + msg: + contain_reserved: "tags recomendadas não podem conter tags reservadas" + required_tag: + title: Definir tags necessárias + label: Definir "Tags recomendadas" como tags necessárias + text: "Every new question must have ao menos one recommend tag." + reserved_tags: + label: Reserved Marcadores + text: "Tags reservadas só podem ser usadas pelo moderador." + image_size: + label: Tamanho máximo da imagem (MB) + text: "O tamanho máximo para upload de imagem." + attachment_size: + label: Tamanho máximo do anexo (MB) + text: "O tamanho máximo para o carregamento de arquivos anexados." + image_megapixels: + label: Máximo megapíxels da imagem + text: "Número máximo de megapixels permitido para uma imagem." + image_extensions: + label: Extensões de imagens autorizadas + text: "Uma lista de extensões de arquivo permitidas para exibição de imagens, separadas por vírgula." + attachment_extensions: + label: Extensões autorizadas para anexos + text: "Uma lista de extensões de arquivo permitidas para carregamento, separar por vírgula. AVISO: permitir o carregamento pode causar problemas de segurança." + seo: + page_title: SEO + permalink: + label: Link permanente + text: Custom URL structures can improve the usability, and forward-compatibility of a sua links. + robots: + label: robos.txt + text: Isto irá substituir permanentemente quaisquer configurações do site relacionadas. + themes: + page_title: Temas + themes: + label: Temas + text: Selecionar um tema existente. + color_scheme: + label: Esquema de cores + navbar_style: + label: Navbar background style + primary_color: + label: Cor primária + text: Modifica as cores usadas por seus temas + css_and_html: + page_title: CSS e HTML + custom_css: + label: CSS Personalizado + text: > + + head: + label: Cabeçalho + text: > + + header: + label: Cabeçalho + text: > + + footer: + label: Rodapé + text: Isto será inserido antes de </body>. + sidebar: + label: Barra lateral + text: Isto irá inserir na barra lateral. + login: + page_title: Entrar + membership: + title: Afiliação + label: Permitir novas inscrições + text: Desligue para impedir que alguém crie uma nova conta. + email_registration: + title: Registrar e-mail + label: Permitir registro utilizando e-mail + text: Desative para impedir que qualquer pessoa crie uma nova conta por e-mail. + allowed_email_domains: + title: Domínios de e-mail permitidos + text: Domínios de e-mail com os quais os usuários devem registrar contas. Um domínio por linha. Ignorado quando vazio. + private: + title: Privado + label: Login requirido + text: Somente usuários conectados podem acessar esta comunidade. + password_login: + title: Login com senha + label: Permitir login por e-mail e senha + text: "AVISO: Se desativar, você pode ser incapaz de efetuar login se você não tiver configurado anteriormente outro método de login." + installed_plugins: + title: Extensões instaladas + plugin_link: Plugins ampliam e expandem a funcionalidade. Você pode encontrar plugins no <1>Repositório de Plugins. + filter: + all: Todos + active: Ativo + inactive: Inativo + outdated: Desactualizado + plugins: + label: Extensões + text: Selecionar uma extensão existente. + name: Nome + version: Versão + status: Estado + action: Ação + deactivate: Desativar + activate: Ativado + settings: Configurações + settings_users: + title: Usuários + avatar: + label: Avatar padrão + text: Para usuários sem um avatar personalizado próprio. + gravatar_base_url: + label: Gravatar Base URL + text: URL da API do provedor Gravatar ignorado quando vazio. + profile_editable: + title: Perfil editável + allow_update_display_name: + label: Permitir que os usuários mudem seus nomes de exibição + allow_update_username: + label: Permitem que os usuário mudem seus nomes de usuário + allow_update_avatar: + label: Permitem que os usuário mudem a sua imagem de perfil + allow_update_bio: + label: Permitir que os usuários mudem suas descrições + allow_update_website: + label: Permitir que os usuários mudem seus web-sites + allow_update_location: + label: Permitir que usuários alterem suas localizações + privilege: + title: Privilégios + level: + label: Nível de reputação necessário + text: Escolha a reputação necessária para os privilégios + msg: + should_be_number: o valor de entrada deve ser número + number_larger_1: número deve ser igual ou maior que 1 + badges: + action: Ação + active: Ativo + activate: Ativado + all: Todos + awards: Prêmios + deactivate: Desativar + filter: + placeholder: Filtrar por nome, badge:id + group: Grupo + inactive: Inativo + name: Nome + show_logs: Mostrar registros + status: Status + title: Emblemas + form: + optional: (opcional) + empty: não pode ser vazio + invalid: é inválido + btn_submit: Salvar + not_found_props: "Propriedade requerida {{ key }} não encontrada." + select: Selecionar + page_review: + review: Revisar + proposed: proposto + question_edit: Editar pergunta + answer_edit: Editar resposta + tag_edit: Editar marcador + edit_summary: Editar descrição + edit_question: Editar pergunta + edit_answer: Editar resposta + edit_tag: Editar marcador + empty: Nenhuma tarefa de revisão restante. + approve_revision_tip: Você aprova esta revisão? + approve_flag_tip: Você aprova esta sinalização? + approve_post_tip: Você aprova esta publicação? + approve_user_tip: Você aprova este usuário? + suggest_edits: Edições sugeridas + flag_post: Post sinalizado + flag_user: Sinalizar usuário + queued_post: Publicação na fila + queued_user: Usuário na fila + filter_label: Tipo + reputation: reputação + flag_post_type: Sinalizou esta publicação como {{ type }}. + flag_user_type: Sinalizou este usuário como {{ type }}. + edit_post: Editar publicação + list_post: Listar postagem + unlist_post: Remover postagem da lista + timeline: + undeleted: não removido + deleted: removido + downvote: voto negativo + upvote: voto positivo + accept: aceito + cancelled: cancelado + commented: comentado + rollback: reversão + edited: editado + answered: respondido + asked: perguntado + closed: fechado + reopened: reaberto + created: criado + pin: fixado + unpin: desafixado + show: listadas + hide: não listado + title: "Histórico para" + tag_title: "Título para" + show_votes: "Mostrar Votos" + n_or_a: Não aplicável + title_for_question: "Título para" + title_for_answer: "Título para resposta {{ title }} por {{ author }}" + title_for_tag: "Título para marcador" + datetime: Data e hora + type: Tipo + by: Por + comment: Comentário + no_data: "Não conseguimos encontrar nada." + users: + title: Usuários + users_with_the_most_reputation: Usuários com maior pontuação + users_with_the_most_vote: Usuários que mais votaram + staffs: Nossos colaboradores + reputation: reputação + votes: votos + prompt: + leave_page: Tem a certeza que quer sair desta página? + changes_not_save: Suas alterações não podem ser salvas. + draft: + discard_confirm: Tem certeza que deseja descartar o rascunho? + messages: + post_deleted: Esta publicação foi removida. + post_cancel_deleted: Esta postagem foi restaurada. + post_pin: Esta publicação foi fixada. + post_unpin: Esta postagem foi desafixada. + post_hide_list: Esta postagem foi ocultada da lista. + post_show_list: Esta postagem foi exibida à lista. + post_reopen: Esta publicação foi re-aberta. + post_list: Esta postagem foi listada. + post_unlist: Esta publicação foi removida da lista. + post_pending: A sua postagem está aguardando revisão. Ela ficará visível depois que for aprovada. + post_closed: Esta postagem foi fechada. + answer_deleted: Esta resposta foi excluída. + answer_cancel_deleted: Esta resposta foi restaurada. + change_user_role: O papel deste usuário foi alterado. + user_inactive: Este usuário já está inativo. + user_normal: Este usuário já está normal. + user_suspended: Este usuário foi suspenso. + user_deleted: Este usuário foi removido. + badge_activated: Este emblema foi ativado. + badge_inactivated: Este emblema foi desativado. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/ro_RO.yaml b/i18n/ro_RO.yaml new file mode 100644 index 000000000..85a9c6f6d --- /dev/null +++ b/i18n/ro_RO.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Succes. + unknown: + other: Eroare necunoscută. + request_format_error: + other: Formatul cererii nu este valid. + unauthorized_error: + other: Neautorizat. + database_error: + other: Eroare la serverul de date. + forbidden_error: + other: Interzis. + duplicate_request_error: + other: Trimitere dublă. + action: + report: + other: Steag + edit: + other: Editează + delete: + other: Ștergere + close: + other: Închide + reopen: + other: Redeschidere + forbidden_error: + other: Interzis. + pin: + other: Fixează + hide: + other: Dezlistare + unpin: + other: Anulați fixarea + show: + other: Listă + invite_someone_to_answer: + other: Editează + undelete: + other: Restabilește + merge: + other: Îmbinare + role: + name: + user: + other: Utilizator + admin: + other: Administrator + moderator: + other: Moderator + description: + user: + other: Implicit fără acces special. + admin: + other: Ai puterea deplină de a accesa site-ul. + moderator: + other: Are acces la toate postările cu excepţia setărilor administratorului. + privilege: + level_1: + description: + other: Nivel 1 (mai puțină reputație pentru echipa privată, grup) + level_2: + description: + other: Nivelul 2 (reputație scăzută necesară pentru comunitatea de pornire) + level_3: + description: + other: Nivelul 3 (reputație ridicată necesară pentru comunitatea mature) + level_custom: + description: + other: Nivel personalizat + rank_question_add_label: + other: Întreabă ceva + rank_answer_add_label: + other: Scrie răspunsul + rank_comment_add_label: + other: Scrie comentariu + rank_report_add_label: + other: Steag + rank_comment_vote_up_label: + other: Votează comentariul + rank_link_url_limit_label: + other: Postează mai mult de 2 link-uri simultan + rank_question_vote_up_label: + other: Votează întrebarea + rank_answer_vote_up_label: + other: Votează răspunsul + rank_question_vote_down_label: + other: Votează întrebarea ca negativa + rank_answer_vote_down_label: + other: Voteaza răspunsul ca negativ + rank_invite_someone_to_answer_label: + other: Invită pe cineva să răspundă + rank_tag_add_label: + other: Creează o etichetă nouă + rank_tag_edit_label: + other: Editați descrierea etichetei (este nevoie de revizuire) + rank_question_edit_label: + other: Editați altă întrebare (este nevoie de revizuire) + rank_answer_edit_label: + other: Editați altă întrebare (este nevoie de revizuire) + rank_question_edit_without_review_label: + other: Editează întrebarea celuilalt fără revizuire + rank_answer_edit_without_review_label: + other: Editează întrebarea celuilalt fără revizuire + rank_question_audit_label: + other: Revizuiește editarea întrebărilor + rank_answer_audit_label: + other: Revizuiește editările răspunsurilor + rank_tag_audit_label: + other: Revizuiește editarea etichetelor + rank_tag_edit_without_review_label: + other: Editează descrierea etichetei fără revizuire + rank_tag_synonym_label: + other: Gestionează sinonimele etichetelor + email: + other: E-mail + e_mail: + other: E-mail + password: + other: Parolă + pass: + other: Parolă + old_pass: + other: Parolă actuală + original_text: + other: Acest articol + email_or_password_wrong_error: + other: E-mailul și parola nu se potrivesc. + error: + common: + invalid_url: + other: URL invalid. + status_invalid: + other: Stare nevalidă. + password: + space_invalid: + other: Parola nu poate conține spații. + admin: + cannot_update_their_password: + other: Nu vă puteți modifica parola. + cannot_edit_their_profile: + other: Nu vă puteți modifica profilul. + cannot_modify_self_status: + other: Nu vă puteți modifica starea. + email_or_password_wrong: + other: E-mailul și parola nu se potrivesc. + answer: + not_found: + other: Răspunsul nu a fost găsit. + cannot_deleted: + other: Nu există permisiunea de ștergere. + cannot_update: + other: Nu există permisiunea de ștergere. + question_closed_cannot_add: + other: Întrebările sunt închise şi nu pot fi adăugate. + content_cannot_empty: + other: Conținutul răspunsului nu poate fi gol. + comment: + edit_without_permission: + other: Comentariul nu poate fi editat. + not_found: + other: Comentariul nu a fost găsit. + cannot_edit_after_deadline: + other: Comentariul a durat prea mult pentru a fi modificat. + content_cannot_empty: + other: Conținutul comentariului nu poate fi gol. + email: + duplicate: + other: Email-ul există deja. + need_to_be_verified: + other: E-mailul trebuie verificat. + verify_url_expired: + other: Adresa de e-mail verificată a expirat, vă rugăm să retrimiteți e-mailul. + illegal_email_domain_error: + other: E-mailul nu este permis din acel domeniu de e-mail. Vă rugăm să folosiți altul. + lang: + not_found: + other: Fișierul de limbă nu a fost găsit. + object: + captcha_verification_failed: + other: Captcha este greșit. + disallow_follow: + other: Nu vă este permis să urmăriți. + disallow_vote: + other: Nu ai permisiunea de a vota. + disallow_vote_your_self: + other: Nu poți vota pentru propria ta postare. + not_found: + other: Obiectul nu a fost găsit. + verification_failed: + other: Verificarea a eșuat. + email_or_password_incorrect: + other: E-mailul și parola nu se potrivesc. + old_password_verification_failed: + other: Verificarea parolei vechi a eșuat + new_password_same_as_previous_setting: + other: Noua parolă este identică cu cea anterioară. + already_deleted: + other: Acest articol a fost șters. + meta: + object_not_found: + other: Nu s-a găsit obiectul Meta + question: + already_deleted: + other: Această postare a fost ștearsă. + under_review: + other: Articolul tău este în așteptare. Acesta va fi vizibil după ce a fost aprobat. + not_found: + other: Întrebarea nu a fost găsită. + cannot_deleted: + other: Nu există permisiunea de ștergere. + cannot_close: + other: Nu există permisiunea de a închide. + cannot_update: + other: Nu aveți permisiunea de a actualiza. + content_cannot_empty: + other: Conținutul nu poate fi gol. + rank: + fail_to_meet_the_condition: + other: Rangul de reputaţie nu îndeplineşte condiţia. + vote_fail_to_meet_the_condition: + other: Mulțumim pentru feedback. Aveți nevoie cel puțin de reputația {{.Rank}} pentru a vota. + no_enough_rank_to_operate: + other: Aveți nevoie cel puțin de reputația {{.Rank}} pentru a face asta. + report: + handle_failed: + other: Procesarea raportării a eșuat. + not_found: + other: Raportul nu a fost găsit. + tag: + already_exist: + other: Eticheta există deja. + not_found: + other: Eticheta nu a fost găsită. + recommend_tag_not_found: + other: Eticheta recomandată nu există. + recommend_tag_enter: + other: Te rugăm să introduci cel puțin o etichetă necesară. + not_contain_synonym_tags: + other: Nu trebuie să conțină etichete sinonime. + cannot_update: + other: Nu aveți permisiunea de a actualiza. + is_used_cannot_delete: + other: Nu puteți șterge o etichetă care este în uz. + cannot_set_synonym_as_itself: + other: Nu se poate seta sinonimul etichetei curente ca atare. + smtp: + config_from_name_cannot_be_email: + other: Numele nu poate fi o adresă de e-mail. + theme: + not_found: + other: Tema nu a fost găsită. + revision: + review_underway: + other: Nu se poate edita momentan, există o versiune în coada de revizuire. + no_permission: + other: Nu ai permisiunea de a revizui. + user: + external_login_missing_user_id: + other: Platforma terță nu oferă un Id de utilizator unic, deci nu vă puteți autentifica, contactați administratorul site-ului. + external_login_unbinding_forbidden: + other: Vă rugăm să setaţi o parolă de conectare pentru contul dumneavoastră înainte de a elimina această autentificare. + email_or_password_wrong: + other: + other: E-mailul și parola nu se potrivesc. + not_found: + other: Utilizatorul nu a fost găsit. + suspended: + other: Utilizatorul a fost suspendat. + username_invalid: + other: Numele de utilizator nu este valid. + username_duplicate: + other: Numele de utilizator este deja luat. + set_avatar: + other: Setarea avatarului a eșuat. + cannot_update_your_role: + other: Nu vă puteți modifica rolul. + not_allowed_registration: + other: În prezent, site-ul nu este deschis pentru înregistrare. + not_allowed_login_via_password: + other: În prezent, site-ul nu este permis să se autentifice prin parolă. + access_denied: + other: Acces Blocat + page_access_denied: + other: Nu aveți acces la această pauză. + add_bulk_users_format_error: + other: "{{.Field}} format lângă '{{.Content}}' la linia {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Numărul de utilizatori pe care îi adăugați odată trebuie să fie în intervalul 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Citirea configurației a eșuat + database: + connection_failed: + other: Conexiunea la baza de date a eșuat + create_table_failed: + other: Crearea tabelului a eșuat + install: + create_config_failed: + other: Nu se poate crea fișierul config.yaml. + upload: + unsupported_file_format: + other: Format de fișier incompatibil. + site_info: + config_not_found: + other: Configurarea site-ului nu a fost găsită. + badge: + object_not_found: + other: Nu s-a găsit obiectul Insignă + reason: + spam: + name: + other: nedorite + desc: + other: Acest post este o reclamă sau un vandalism. Nu este util sau relevant pentru subiectul actual. + rude_or_abusive: + name: + other: nepoliticos sau abuziv + desc: + other: "O persoană rezonabilă ar considera acest conținut nepotrivit pentru un discurs respectuos." + a_duplicate: + name: + other: un duplicat + desc: + other: Această întrebare a fost adresată înainte şi are deja un răspuns. + placeholder: + other: Introduceți link-ul de întrebare existent + not_a_answer: + name: + other: nu este un răspuns + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: nu mai este necesar + desc: + other: Acest comentariu este învechit, conversaţional sau nu are relevanţă pentru această postare. + something: + name: + other: altceva + desc: + other: Acest post necesită atenție din partea personalului, din alt motiv nemenționat mai sus. + placeholder: + other: Spune-ne ce anume vă îngrijorează + community_specific: + name: + other: un motiv specific comunității + desc: + other: Această întrebare nu corespunde cu ghidul comunității. + not_clarity: + name: + other: are nevoie de detalii sau de claritate + desc: + other: Această întrebare include în prezent mai multe întrebări. Ar trebui să se concentreze asupra unei singure probleme. + looks_ok: + name: + other: arată OK + desc: + other: Această postare este bună și nu este de slabă calitate. + needs_edit: + name: + other: are nevoie de editare și am făcut-o + desc: + other: Îmbunătățește și corectează problemele cu această postare. + needs_close: + name: + other: necesită închidere + desc: + other: La o întrebare închisă nu poți răspunde, dar poți totuși să o editezi, să o votezi și să o comentezi. + needs_delete: + name: + other: necesită ștergere + desc: + other: Această postare va fi ștearsă. + question: + close: + duplicate: + name: + other: nedorite + desc: + other: Această întrebare a fost adresată înainte şi are deja un răspuns. + guideline: + name: + other: un motiv specific comunității + desc: + other: Această întrebare nu corespunde cu ghidul comunității. + multiple: + name: + other: necesită detalii sau claritate + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: altceva + desc: + other: Acest post necesită un alt motiv care nu este listat mai sus. + operation_type: + asked: + other: întrebat + answered: + other: răspunse + modified: + other: modificat + deleted_title: + other: Întrebare ștearsă + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: întrebarea actualizată + answer_the_question: + other: întrebare răspunsă + update_answer: + other: răspuns actualizat + accept_answer: + other: răspuns acceptat + comment_question: + other: întrebare comentată + comment_answer: + other: răspuns comentat + reply_to_you: + other: ți-a răspuns + mention_you: + other: te-a menționat + your_question_is_closed: + other: Întrebarea dumneavoastră a fost închisă + your_question_was_deleted: + other: Întâlnirea dumneavoastră a fost ştearsă + your_answer_was_deleted: + other: Răspunsul dumneavoastră a fost șters + your_comment_was_deleted: + other: Contul dumneavoastră a fost șters + up_voted_question: + other: votează întrebarea + down_voted_question: + other: votează întrebarea negativ + up_voted_answer: + other: votează răspunsul + down_voted_answer: + other: răspuns negativ + up_voted_comment: + other: votează comentariul + invited_you_to_answer: + other: te-a invitat să răspunzi + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirmați noua dvs. adresă de e-mail" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} vă invită să răspundeți" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} a răspuns la întrebarea dvs" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] Întrebare nouă: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Resetare parolă" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirmă noul tău cont" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test de e-mail" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: votat + upvoted: + other: vot pozitiv + downvote: + other: vot negativ + downvoted: + other: vot negativ + accept: + other: acceptat + accepted: + other: acceptat + edit: + other: editează + review: + queued_post: + other: Posturi în așteptare + flagged_post: + other: Postare marcată + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Contribuții restante în prima lor lună. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Distribuire grozavă + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Cum se formatează + desc: >- + + pagination: + prev: Înapoi + next: Înainte + page_title: + question: Întrebare + questions: Întrebări + tag: Etichetă + tags: Etichete + tag_wiki: etichetă wiki + create_tag: Creați etichetă + edit_tag: Modificați eticheta + ask_a_question: Create Question + edit_question: Editați întrebarea + edit_answer: Editaţi răspunsul + search: Caută + posts_containing: Posturi care conțin + settings: Setări + notifications: Notificări + login: Conectează-te + sign_up: Înregistrează-te + account_recovery: Recuperarea contului + account_activation: Activare cont + confirm_email: Confirmare e-mail + account_suspended: Cont suspendat + admin: Administrator + change_email: Modifică E-mail + install: Instalează Answer + upgrade: Actualizare Answer + maintenance: Mentenanță website + users: Utilizatori + oauth_callback: Se procesează + http_404: Eroare HTTP 404 + http_50X: Eroare HTTP 500 + http_403: Eroare HTTP 403 + logout: Deconectare + notifications: + title: Notificări + inbox: Mesaje primite + achievement: Realizări + new_alerts: Alerte noi + all_read: Marchează totul ca fiind citit + show_more: Arată mai mult + someone: Cineva + inbox_type: + all: Toate + posts: Postări + invites: Invitați + votes: Voturi + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Contul dumneavoastră a fost suspendat + until_time: "Contul dumneavoastră a fost suspendat până la {{ time }}." + forever: Acest utilizator a fost suspendat pentru totdeauna. + end: Această întrebare nu corespunde cu ghidul comunității. + contact_us: Contactați-ne + editor: + blockquote: + text: Citat + bold: + text: Bolt + chart: + text: Diagramă + flow_chart: Diagrama fluxului + sequence_diagram: Diagrama secvenței + class_diagram: Diagrama clasei + state_diagram: Diagrama stării + entity_relationship_diagram: Diagrama relației entității + user_defined_diagram: Diagramă definită de utilizator + gantt_chart: Grafic Gantt + pie_chart: Grafic circular + code: + text: Exemplu de cod + add_code: Adaugă exemplu de cod + form: + fields: + code: + label: Cod + msg: + empty: Corpul mesajului trebuie să conțină text. + language: + label: Limbă + placeholder: Detectare automată + btn_cancel: Anulați + btn_confirm: Adaugă + formula: + text: Formulă + options: + inline: Formula inline + block: Formula blocului + heading: + text: Titlu + options: + h1: Titlu 1 + h2: Titlu 2 + h3: Titlu 3 + h4: Titlu 4 + h5: Titlu 5 + h6: Titlu 6 + help: + text: Ajutor + hr: + text: Linie orizontală + image: + text: Imagine + add_image: Adaugă imagine + tab_image: Incarca poza + form_image: + fields: + file: + label: Fișier imagine + btn: Selectați imaginea + msg: + empty: Fișierul nu poate fi gol. + only_image: Sunt permise doar fișierele imagine. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Descriere + tab_url: URL-ul imaginii + form_url: + fields: + url: + label: URL-ul imaginii + msg: + empty: URL-ul imaginii nu poate fi gol. + name: + label: Descriere + btn_cancel: Anulați + btn_confirm: Adaugă + uploading: Se încarcă + indent: + text: Indentare + outdent: + text: Outdent + italic: + text: Accentuare + link: + text: Hyperlink + add_link: Adaugă hiperlink + form: + fields: + url: + label: URL + msg: + empty: URL-ul nu poate fi gol. + name: + label: Descriere + btn_cancel: Anulează + btn_confirm: Adaugă + ordered_list: + text: Listă numerotată + unordered_list: + text: Listă cu marcatori + table: + text: Tabelă + heading: Titlu + cell: Celulă + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Închid această postare ca... + btn_cancel: Anulează + btn_submit: Trimiteți + remark: + empty: Nu poate fi lăsat necompletat. + msg: + empty: Te rugăm să selectezi un motiv. + report_modal: + flag_title: Fac un semnal de alarmă pentru a raporta acest post ca... + close_title: Închid această postare ca... + review_question_title: Revizuiește întrebarea + review_answer_title: Revizuiește răspunsul + review_comment_title: Revizuiește comentariul + btn_cancel: Anulează + btn_submit: Trimiteți + remark: + empty: Nu poate fi lăsat necompletat. + msg: + empty: Te rugăm să selectezi un motiv. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Creează o etichetă nouă + form: + fields: + display_name: + label: Nume afișat + msg: + empty: Numele afișat nu poate fi gol. + range: Nume afișat până la 35 de caractere. + slug_name: + label: Slug URL + desc: Slug-ul URL pana la 35 de caractere. + msg: + empty: Slug-ul URL nu poate fi gol. + range: Slug-ul URL pana la 35 de caractere. + character: URL-ul slug conţine un set de caractere nepermis. + desc: + label: Descriere + revision: + label: Versiunea + edit_summary: + label: Editează sumarul + placeholder: >- + Explicați pe scurt modificările (ortografie corectată, gramatică fixată, formatare îmbunătățită) + btn_cancel: Anulează + btn_submit: Trimiteți + btn_post: Postează o nouă etichetă + tag_info: + created_at: Creat + edited_at: Editat + history: Istoric + synonyms: + title: Sinonime + text: Următoarele etichete vor fi păstrate la + empty: Nu s-au găsit sinonime. + btn_add: Adaugă un sinonim + btn_edit: Editează + btn_save: Salvează + synonyms_text: Următoarele etichete vor rămâne la + delete: + title: Șterge această etichetă + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Sunteţi sigur că doriţi să ştergeţi? + close: Închide + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Editează eticheta + default_reason: Editare etichetă + default_first_reason: Adaugă etichetă + btn_save_edits: Salvați modificările + btn_cancel: Anulați + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, AAAA [at] HH:mm" + now: acum + x_seconds_ago: "acum {{count}} sec" + x_minutes_ago: "acum {{count}} min" + x_hours_ago: "acum {{count}} ore" + hour: oră + day: zi + hours: ore + days: zile + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Adaugă comentariu + reply_to: Raspunde la + btn_reply: Răspunde + btn_edit: Editează + btn_delete: Ștergeți + btn_flag: Marcaj + btn_save_edits: Salvați modificările + btn_cancel: Anulați + show_more: "{{count}} alte comentarii" + tip_question: >- + Utilizați comentariile pentru a solicita mai multe informații sau pentru a sugera îmbunătățiri. Evitați răspunsul la întrebări în comentarii. + tip_answer: >- + Utilizați comentarii pentru a răspunde la alți utilizatori sau pentru a le notifica modificările. Dacă adăugați informații noi, editați postarea în loc să comentați. + tip_vote: Adaugă ceva util postării + edit_answer: + title: Editaţi răspunsul + default_reason: Editați răspunsul + default_first_reason: Adăugare răspuns + form: + fields: + revision: + label: Revizuire + answer: + label: Răspuns + feedback: + characters: conţinutul trebuie să aibă cel puţin 6 caractere. + edit_summary: + label: Editează sumarul + placeholder: >- + Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită) + btn_save_edits: Salvați modificările + btn_cancel: Anulează + tags: + title: Etichete + sort_buttons: + popular: Popular + name: Nume + newest: Cele mai noi + button_follow: Urmărește + button_following: Urmăriți + tag_label: întrebări + search_placeholder: Filtrare după numele etichetei + no_desc: Această echipă nu are o descriere. + more: Mai multe + wiki: Wiki + ask: + title: Create Question + edit_title: Editați întrebarea + default_reason: Editați întrebarea + default_first_reason: Create question + similar_questions: Întrebări similare + form: + fields: + revision: + label: Revizuire + title: + label: Titlu + placeholder: What's your topic? Be specific. + msg: + empty: Titlul nu poate fi gol. + range: Titlu de până la 150 de caractere + body: + label: Corp + msg: + empty: Corpul mesajului trebuie să conțină text. + tags: + label: Etichete + msg: + empty: Etichetele nu pot fi goale. + answer: + label: Răspuns + msg: + empty: Răspunsul nu poate fi gol. + edit_summary: + label: Editează sumarul + placeholder: >- + Explicați pe scurt modificările (ortografie corectată, gramatică fixă, formatare îmbunătățită) + btn_post_question: Postează întrebarea ta + btn_save_edits: Salvați modificările + answer_question: Răspundeți la propria întrebare + post_question&answer: Postează-ți întrebarea și răspunsul + tag_selector: + add_btn: Adaugă etichetă + create_btn: Creează o etichetă nouă + search_tag: Căutare etichetă + hint: "Describe what your content is about, at least one tag is required." + no_result: Nicio etichetă potrivită + tag_required_text: Etichetă necesară (cel puțin una) + header: + nav: + question: Întrebări + tag: Etichete + user: Utilizatori + badges: Badges + profile: Profil + setting: Setări + logout: Deconectaţi-vă + admin: Administrator + review: Recenzie + bookmark: Semne de carte + moderation: Moderare + search: + placeholder: Caută + footer: + build_on: >- + Susținut de <1> Apache Răspuns - software-ul open-source care asigură Q&A comunități.
Făcut cu dragoste ©️ {{cc}}. + upload_img: + name: Schimbare + loading: încarcare... + pic_auth_code: + title: Captcha + placeholder: Introdu textul de mai sus + msg: + empty: Captcha nu poate fi gol. + inactive: + first: >- + Ești aproape gata! Am trimis un e-mail de activare la {{mail}}. Te rugăm să urmezi instrucțiunile din e-mail pentru a-ți activa contul. + info: "Dacă nu ajunge, verifică folderul Spam." + another: >- + Ți-am trimis un alt e-mail de activare la {{mail}}. Poate dura câteva minute până ajuns; asiguraţi-vă că verificaţi folderul Spam. + btn_name: Retrimitere link de activare + change_btn_name: Schimbați e-mailul + msg: + empty: Nu poate fi lăsat necompletat. + resend_email: + url_label: Sunteţi sigur că doriţi să retrimiteţi e-mailul de activare? + url_text: De asemenea, puteți da link-ul de activare de mai sus utilizatorului. + login: + login_to_continue: Conectează-te pentru a continua + info_sign: Nu ai un cont? Înregistrează-te + info_login: Ai deja un cont? <1>Autentifică-te + agreements: Prin înregistrare, ești de acord cu <1>politica de confidențialitate și <3>termenii și condițiile de utilizare. + forgot_pass: Ai uitat parola? + name: + label: Nume + msg: + empty: Câmpul Nume trebuie completat. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: E-mail + msg: + empty: Câmpul e-mail nu poate fi gol. + password: + label: Parolă + msg: + empty: Parola nu poate fi goală. + different: Parolele introduse pe ambele părți sunt incompatibile + account_forgot: + page_title: Ati uitat parola + btn_name: Trimite-mi e-mail de recuperare + send_success: >- + Dacă un cont corespunde cu {{mail}}, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp. + email: + label: E-mail + msg: + empty: Câmpul e-mail nu poate fi gol. + change_email: + btn_cancel: Anulați + btn_update: Actualizare adresă de e-mail + send_success: >- + Dacă un cont corespunde cu {{mail}}, ar trebui să primiți un e-mail cu instrucțiuni despre cum să resetați parola în scurt timp. + email: + label: E-mail nou + msg: + empty: Câmpul e-mail nu poate fi gol. + oauth: + connect: Conectează-te cu {{ auth_name }} + remove: Elimină {{ auth_name }} + oauth_bind_email: + subtitle: Adăugați un e-mail de recuperare la contul dvs. + btn_update: Actualizare adresă de e-mail + email: + label: E-mail + msg: + empty: E-mail-ul nu poate fi gol. + modal_title: E-mail deja existent. + modal_content: Această adresă de e-mail este deja înregistrată. Sigur doriți să vă conectați la contul existent? + modal_cancel: Schimbați e-mailul + modal_confirm: Conectează-te la contul existent + password_reset: + page_title: Resetează parola + btn_name: Resetează-mi parola + reset_success: >- + Ați schimbat cu succes parola; veți fi redirecționat către pagina de conectare. + link_invalid: >- + Ne pare rău, acest link de resetare a parolei nu mai este valabil. Poate că parola este deja resetată? + to_login: Continuă autentificarea în pagină + password: + label: Parolă + msg: + empty: Parola nu poate fi goală. + length: Lungimea trebuie să fie între 8 și 32 + different: Parolele introduse pe ambele părți sunt incompatibile + password_confirm: + label: Confirmă parola nouă + settings: + page_title: Setări + goto_modify: Du-te pentru a modifica + nav: + profile: Profil + notification: Notificări + account: Cont + interface: Interfață + profile: + heading: Profil + btn_name: Salvează + display_name: + label: Nume afișat + msg: Numele afișat nu poate fi gol. + msg_range: Display name must be 2-30 characters in length. + username: + label: Nume de utilizator + caption: Oamenii te pot menționa ca "@utilizator". + msg: Numele de utilizator nu poate fi gol. + msg_range: Username must be 2-30 characters in length. + character: 'Trebuie să utilizați setul de caractere "a-z", "0-9", " - . _"' + avatar: + label: Imaginea de profil + gravatar: Gravatar + gravatar_text: Poți schimba imaginea pe + custom: Personalizat + custom_text: Poți să încarci imaginea. + default: Sistem + msg: Te rugăm să încarci un avatar + bio: + label: Despre mine + website: + label: Website + placeholder: "https://exemplu.com" + msg: Format incorect pentru website + location: + label: Locație + placeholder: "Oraş, Ţară" + notification: + heading: Notificări prin e-mail + turn_on: Pornire + inbox: + label: Notificări primite + description: Răspunde la întrebări, comentarii, invitații și multe altele. + all_new_question: + label: Adauga o intrebare noua + description: Primiți notificări despre toate întrebările noi. Până la 50 de întrebări pe săptămână. + all_new_question_for_following_tags: + label: Toate întrebările noi pentru etichetele următoare + description: Primiți notificări despre întrebări noi pentru următoarele etichete. + account: + heading: Cont + change_email_btn: Schimbați e-mailul + change_pass_btn: Schimbați parola + change_email_info: >- + Am trimis un e-mail la acea adresă. Vă rugăm să urmați instrucțiunile de confirmare. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Parolă actuală + msg: Parola nu poate fi goală. + password_title: Parolă + current_pass: + label: Parola curentă + msg: + empty: Parola curentă nu poate fi goală. + length: Lungimea trebuie să fie între 8 și 32. + different: Cele două parole introduse nu se potrivesc. + new_pass: + label: Parola nouă + pass_confirm: + label: Confirmă parola nouă + interface: + heading: Interfață + lang: + label: Limba interfeței + text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina. + my_logins: + title: Autentificările mele + label: Autentifică-te sau înregistrează-te pe acest site folosind aceste conturi. + modal_title: Elimină autentificarea + modal_content: Sunteţi sigur că doriţi să eliminaţi această autentificare din contul dumneavoastră? + modal_confirm_btn: Eliminare + remove_success: Eliminată cu succes + toast: + update: actualizare reușită + update_password: Parola schimbata cu succes. + flag_success: Mulțumim pentru marcare. + forbidden_operate_self: Interzis să operezi singur + review: Revizuirea ta va arăta după recenzie. + sent_success: Trimis cu succes + related_question: + title: Related + answers: răspunsuri + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Persoane întrebate + desc: Invită persoane care crezi că știu răspunsul. + invite: Invită să răspundă + add: Adaugă persoane + search: Caută persoane + question_detail: + action: Acţiune + Asked: Întrebat + asked: întrebat + update: Modificat + edit: editat + commented: commented + Views: Văzute + Follow: Urmărește + Following: Urmăriți + follow_tip: Urmărește această întrebare pentru a primi notificări + answered: răspunse + closed_in: Închis în + show_exist: Arată întrebarea existentă. + useful: Utilă + question_useful: Acest lucru este util și clar + question_un_useful: Nu este clar sau nu este util + question_bookmark: Marchează această întrebare + answer_useful: Este util + answer_un_useful: Nu este util + answers: + title: Răspunsuri + score: Scor + newest: Cele mai noi + oldest: Cel mai vechi + btn_accept: Acceptă + btn_accepted: Acceptă + write_answer: + title: Răspunsul tău + edit_answer: Editează răspunsul meu existent + btn_name: Postează răspunsul tău + add_another_answer: Adaugă un alt răspuns + confirm_title: Continuă să răspunzi + continue: Continuare + confirm_info: >- +

Sunteţi sigur că doriţi să adăugaţi un alt răspuns?

Puteţi folosi link-ul de editare pentru a perfecţiona şi îmbunătăţi răspunsul existent, în schimb.

+ empty: Răspunsul nu poate fi gol. + characters: conţinutul trebuie să aibă cel puţin 6 caractere. + tips: + header_1: Îți mulțumim pentru răspuns + li1_1: Asigurați-vă că răspundeți la întrebarea. Furnizați detalii și împărtășiți cercetările dvs. + li1_2: Faceți o copie de rezervă cu referințe sau experiență personală. + header_2: Dar evită... + li2_1: Solicită ajutor, caută clarificări sau răspunsuri la alte răspunsuri. + reopen: + confirm_btn: Redeschide + title: Redeschide această postare + content: Sunteţi sigur că doriţi să redeschideţi? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Fixează această postare + content: Sunteţi sigur că doriţi să fixaţi la nivel global? Acest post va apărea în partea de sus a tuturor listelor de postări. + confirm_btn: Fixează + delete: + title: Șterge această postare + question: >- + Nu recomandăm ștergerea întrebărilor cu răspunsuri deoarece acest lucru privează viitorii cititori de aceste cunoștințe.

Ștergerea repetată a întrebărilor cu răspuns poate duce la blocarea contului dvs. de a întreba. Sigur doriți să ștergeți? + answer_accepted: >- +

Nu recomandăm ștergerea răspunsului acceptat deoarece acest lucru privează viitorii cititori de aceste cunoștințe.

Ștergerea repetată a răspunsurilor acceptate poate duce la blocarea contului dvs. de a răspunde. Sigur doriți să ștergeți? + other: Sunteţi sigur că doriţi să ştergeţi? + tip_answer_deleted: Aceasta postare a fost stearsa + undelete_title: Anulează ștergerea acestei postări + undelete_desc: Sunteți sigur că doriți să adulați ștergerea? + btns: + confirm: Confirmați + cancel: Anulați + edit: Editează + save: Salvează + delete: Ștergeți + undelete: Restabilește + list: List + unlist: Unlist + unlisted: Unlisted + login: Autentifică-te + signup: Înscrieți-vă + logout: Deconectaţi-vă + verify: Verificare + create: Create + approve: Aprobă + reject: Respins + skip: Treci peste + discard_draft: Respingeți draftul + pinned: Fixat + all: Toate + question: Întrebare + answer: Răspuns + comment: Comentariu + refresh: Actualizare + resend: Retrimite + deactivate: Dezactivare + active: Activați + suspend: Suspendați + unsuspend: Anulează suspendare + close: Închide + reopen: Redeschide + ok: OK + light: Luminoasă + dark: Întunecată + system_setting: Setări de sistem + default: Default + reset: Resetează + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Rezultatele căutării + keywords: Cuvinte cheie + options: Opţiuni + follow: Urmărește + following: Urmăriți + counts: "{{count}} rezultatele" + counts_loading: "... Results" + more: Mai mult + sort_btns: + relevance: Relevanță + newest: Cele mai noi + active: Activ + score: Scor + more: Mai mult + tips: + title: Sfaturi de căutare avansate + tag: "<1>[tag] search with a tag" + user: "<1>utilizator:username căutare de către autor" + answer: "<1>răspunsuri:0 întrebări fără răspuns" + score: "<1>scor:3 postări cu un scor de 3+" + question: "<1>este:question întrebări de căutare" + is_answer: "<1>este:răspuner răspunsuri la căutare" + empty: Nu am putut găsi nimic.
Încearcă cuvinte cheie diferite sau mai puţin specifice. + share: + name: Distribuiți + copy: Copiază linkul + via: Distribuie postarea prin... + copied: Copiat + facebook: Partajează pe Facebook + twitter: Share to X + cannot_vote_for_self: Nu poți vota pentru propria ta postare. + modal_confirm: + title: Eroare... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Noul tău cont este confirmat; vei fi redirecționat către pagina de pornire. + link: Continuă la pagina principală + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: E-mailul dvs. a fost actualizat. + confirm_new_email_invalid: >- + Ne pare rău, acest link de confirmare nu mai este valabil. Poate că e-mailul dvs. a fost deja modificat? + unsubscribe: + page_title: Dezabonează-te + success_title: Dezabonare cu succes + success_desc: Ați fost eliminat cu succes din această listă de abonați și nu veți mai primi alte e-mailuri de la noi. + link: Modificați setările + question: + following_tags: Etichete urmărite + edit: Editează + save: Salvează + follow_tag_tip: Urmărește etichetele pentru a curăța lista ta de întrebări. + hot_questions: Întrebări importante + all_questions: Toate întrebările + x_questions: "{{ count }} Întrebări" + x_answers: "{{ count }} răspunsuri" + x_posts: "{{ count }} Posts" + questions: Întrebări + answers: Răspunsuri + newest: Cele mai noi + active: Activ + hot: Hot + frequent: Frequent + recommend: Recommend + score: Scor + unanswered: Fără răspuns + modified: modificat + answered: răspunse + asked: întrebat + closed: închise + follow_a_tag: Urmărește o etichetă + more: Mai multe + personal: + overview: Privire de ansamblu + answers: Răspunsuri + answer: răspuns + questions: Întrebări + question: întrebare + bookmarks: Semne de carte + reputation: Reputație + comments: Comentarii + votes: Voturi + badges: Badges + newest: Cele mai noi + score: Scor + edit_profile: Editare profil + visited_x_days: "{{ count }} zile vizitate" + viewed: Văzute + joined: Înscris + comma: "," + last_login: Văzut + about_me: Despre mine + about_me_empty: "// Salut, Lumea !" + top_answers: Top răspunsuri + top_questions: Top Intrebari + stats: Statistici + list_empty: Nici o postare găsită.
Poate doriţi să selectaţi o filă diferită? + content_empty: No posts found. + accepted: Acceptat + answered: răspunse + asked: întrebat + downvoted: vot negativ + mod_short: MOD + mod_long: Moderatori + x_reputation: reputație + x_votes: voturi primite + x_answers: răspunsuri + x_questions: întrebări + recent_badges: Recent Badges + install: + title: Installation + next: Înainte + done: Finalizat + config_yaml_error: Nu se poate crea fișierul config.yaml. + lang: + label: Vă rugăm să selectați limba + db_type: + label: Motorul Bazei de Date + db_username: + label: Nume de utilizator + placeholder: root + msg: Numele de utilizator nu poate fi gol. + db_password: + label: Parolă + placeholder: root + msg: Parola nu poate fi goală. + db_host: + label: Numele serverului de baze de date + placeholder: "db:3306" + msg: Adresa bazei de date nu poate fi goală. + db_name: + label: Numele bazei de date + placeholder: răspuns + msg: Numele bazei de date nu poate fi gol. + db_file: + label: Fișierul bazei de date + placeholder: /data/answer.db + msg: Fişierul bazei de date nu poate fi gol. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Crează config.yaml + label: Fișierul config.yaml a fost creat. + desc: >- + Puteți crea fișierul <1>config.yaml manual în directorul <1>/var/wwww/xxx/ și inserați următorul text în el. + info: După ce ai făcut asta, apasă butonul "Înainte". + site_information: Informatii site + admin_account: Contul de admin + site_name: + label: Numele site-ului + msg: Numele site-ului nu poate fi gol. + msg_max_length: Numele site-ului trebuie să aibă maximum 30 de caractere lungime. + site_url: + label: URL-ul site-ului + text: Adresa site-ului dvs. + msg: + empty: URL-ul site-ului nu poate fi gol. + incorrect: Format incorect pentru URL-ul site-ului. + max_length: URL-ul site-ului trebuie să aibă maximum 512 caractere lungime. + contact_email: + label: E-mail de contact + text: Adresa de e-mail a persoanei cheie responsabile pentru acest site. + msg: + empty: E-mailul de contact nu poate fi gol. + incorrect: E-mail de contact are un format incorect. + login_required: + label: Privat + switch: Autentificare necesară + text: Numai utilizatorii autentificați pot accesa această comunitate. + admin_name: + label: Nume + msg: Câmpul Nume trebuie completat. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Parolă + text: >- + Veți avea nevoie de această parolă pentru autentificare. Vă rugăm să o păstrați într-o locație sigură. + msg: Parola nu poate fi goală. + msg_min_length: Parola trebuie să aibă cel puțin 8 caractere. + msg_max_length: Parola trebuie să aibă cel puțin 32 caractere. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: E-mail + text: Veţi avea nevoie de acest e-mail pentru a vă autentifica. + msg: + empty: Câmpul e-mail nu poate fi gol. + incorrect: Campul e-mail are formatul incorect. + ready_title: Your site is ready + ready_desc: >- + Dacă te simți vreodată ca și cum ai schimba mai multe setări, vizitează <1>secțiunea de administrare ; găsește-o în meniul site-ului. + good_luck: "Distracție plăcută și noroc!" + warn_title: Atenţie + warn_desc: >- + Fișierul <1>config.yaml există deja. Dacă trebuie să resetați oricare dintre elementele de configurare din acest fișier, vă rugăm să îl ștergeți mai întâi. + install_now: Puteți încerca <1>să instalați acum. + installed: Deja instalat + installed_desc: >- + Se pare că ai instalat deja. Pentru a reinstala vă rugăm să ștergeți mai întâi vechile tabele ale bazei de date. + db_failed: Conexiunea la baza de date a eșuat + db_failed_desc: >- + Acest lucru înseamnă fie că baza de date este în configurația ta <1>. config yaml este incorect sau contactul cu serverul bazei de date nu a putut fi stabilit. Acest lucru ar putea însemna că serverul gazdei nu este în funcțiune. + counts: + views: vizualizări + votes: voturi + answers: răspunsuri + accepted: Acceptat + page_error: + http_error: HTTP - {{ code }} + desc_403: Nu ai permisiunea să accesezi pagina. + desc_404: Din păcate, această pagină nu există. + desc_50X: Serverul a întâmpinat o eroare și nu a putut finaliza cererea. + back_home: Înapoi la pagina principală + page_maintenance: + desc: "Suntem în mentenanță, ne vom întoarce în curând." + nav_menus: + dashboard: Panou de control + contents: Conţinut + questions: Întrebări + answers: Răspunsuri + users: Utilizatori + badges: Badges + flags: Marcaj + settings: Setări + general: General + interface: Interfață + smtp: SMTP + branding: Marcă + legal: Juridic + write: Scrie + tos: Condiții de utilizare + privacy: Confidențialitate + seo: SEO + customize: Personalizează + themes: Teme + login: Autentifică-te + privileges: Privilegii + plugins: Extensii + installed_plugins: Extensii instalate + apperance: Appearance + website_welcome: Bun venit la {{site_name}} + user_center: + login: Autentifică-te + qrcode_login_tip: Vă rugăm să folosiți {{ agentName }} pentru a scana codul QR și a vă autentifica. + login_failed_email_tip: Autentificare eșuată. Vă rugăm să permiteți acestei aplicații să vă acceseze informațiile de e-mail înainte de a încerca din nou. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Administrator + dashboard: + title: Panou de control + welcome: Welcome to Admin! + site_statistics: Statisticile site-ului + questions: "Întrebări:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Răspunsuri:" + comments: "Comentarii:" + votes: "Voturi:" + users: "Utilizatori:" + flags: "Marcaje:" + reviews: "Reviews:" + site_health: Site health + version: "Versiune:" + https: "HTTPS:" + upload_folder: "Director încărcare:" + run_mode: "Modul de rulare:" + private: Privat + public: Public + smtp: "SMTP:" + timezone: "Fusul orar:" + system_info: Informaţii despre sistem + go_version: "Versiune Go:" + database: "Baza de date:" + database_size: "Dimensiune bază de date:" + storage_used: "Spațiu utilizat:" + uptime: "Timpul de funcționare:" + links: Links + plugins: Pluginuri + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Ducumente + feedback: Feedback + support: Suport + review: Recenzie + config: Configurație + update_to: Actualizare la + latest: Recente + check_failed: Verificarea eșuată + "yes": "Da" + "no": "Nu" + not_allowed: Nu este permis + allowed: Permis + enabled: Activat + disabled: Dezactivat + writable: Inscriptibil + not_writable: Neinscriptibil + flags: + title: Steaguri + pending: În așteptare + completed: Finalizată + flagged: Semnalizat + flagged_type: Marcat {{ type }} + created: Creată + action: Actiune + review: Recenzie + user_role_modal: + title: Schimbă rolul utilizatorului la... + btn_cancel: Anulați + btn_submit: Trimiteți + new_password_modal: + title: Setați parola nouă + form: + fields: + password: + label: Parolă + text: Utilizatorul va fi deconectat și trebuie să se conecteze din nou. + msg: Parola trebuie să aibă o lungime de 8-32 caractere. + btn_cancel: Anulați + btn_submit: Trimiteți + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Adaugă un nou utilizator + form: + fields: + users: + label: Adăugare utilizator în bloc + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separați “nume, email, parola” cu virgulă. Un utilizator pe linie. + msg: "Te rugăm să introduci e-mailul utilizatorului, câte unul pe linie." + display_name: + label: Nume afișat + msg: Display name must be 2-30 characters in length. + email: + label: E-mail + msg: E-mail nu este validă. + password: + label: Parolă + msg: Parola trebuie să aibă o lungime de 8-32 caractere. + btn_cancel: Anulați + btn_submit: Trimiteți + users: + title: Utilizatori + name: Nume + email: E-mail + reputation: Reputație + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Stare + role: Rol + action: Acţiune + change: Schimbare + all: Toate + staff: Personal + more: Mai mult + inactive: Inactiv + suspended: Suspendat + deleted: Şters + normal: Normal + Moderator: Moderator + Admin: Administrator + User: Utilizator + filter: + placeholder: "Filtrare după nume, utilizator:id" + set_new_password: Setați parola nouă + edit_profile: Edit profile + change_status: Schimba starea + change_role: Schimbare rol + show_logs: Arată jurnalele + add_user: Adaugă utilizator + deactivate_user: + title: Dezactivare utilizator + content: Un utilizator inactiv trebuie să își revalideze e-mailul. + delete_user: + title: Ștergeți acest utilizator + content: Sunteţi sigur că doriţi să ştergeţi acest utilizator? Acest lucru este permanent! + remove: Eliminaţi acest conţinut + label: Elimină toate întrebările, răspunsurile, comentariile etc. + text: Nu bifați acest lucru dacă doriți să ștergeți doar contul utilizatorului. + suspend_user: + title: Suspendă acest utilizator + content: Un utilizator suspendat nu se poate autentifica. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Întrebări + unlisted: Unlisted + post: Postare + votes: Voturi + answers: Răspunsuri + created: Creată + status: Stare + action: Actiune + change: Schimbare + pending: În așteptare + filter: + placeholder: "Filtrează după titlu, întrebare: id" + answers: + page_title: Răspunsuri + post: Postare + votes: Voturi + created: Creată + status: Stare + action: Acţiune + change: Schimbare + filter: + placeholder: "Filtrează după titlu, întrebare: id" + general: + page_title: General + name: + label: Numele site-ului + msg: Numele site-ului nu poate fi gol. + text: "Numele acestui site, așa cum este folosit în eticheta de titlu." + site_url: + label: URL-ul site-ului + msg: Url-ul site-ului nu poate fi gol. + validate: Introduceți un URL valid. + text: Adresa site-ului dvs. + short_desc: + label: Scurtă descriere a site-ului + msg: Scurtă descriere a site-ului nu poate fi goală. + text: "Scurtă descriere, așa cum este folosit în eticheta titlu pe website." + desc: + label: Descriere site + msg: Descrierea site-ului nu poate fi goală. + text: "Descrie acest site într-o propoziție, așa cum este folosit în tag-ul meta descriere." + contact_email: + label: E-mail de contact + msg: E-mailul de contact nu poate fi gol. + validate: E-mailul de contact nu este valid. + text: Adresa de e-mail a persoanei cheie responsabile pentru acest site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interfață + language: + label: Limba interfeței + msg: Limba interfata nu poate fi goala. + text: Limba interfeței utilizatorului. Se va schimba atunci când se reîmprospătează pagina. + time_zone: + label: Fusul orar + msg: Fusul orar nu poate fi gol. + text: Alege un oraș în același fus orar cu tine. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: De la e-mail + msg: Câmpul e-mail nu poate fi gol. + text: Adresa de e-mail de la care e-mailurile sunt trimise. + from_name: + label: Din numele + msg: Numele nu poate fi gol. + text: Numele de la care sunt trimise e-mailurile. + smtp_host: + label: Gazda SMTP + msg: Gazda SMTP nu poate fi goală. + text: Serverul tau de mail. + encryption: + label: Criptare + msg: Câmpul decriptare nu poate fi gol. + text: Pentru majoritatea serverelor SSL este opțiunea recomandată. + ssl: SSL + tls: TLS + none: Niciuna + smtp_port: + label: Portul SMTP + msg: Portul SMTP trebuie să fie numărul 1 ~ 65535. + text: Portul către serverul de mail. + smtp_username: + label: Utilizatorul SMTP + msg: Numele de utilizator SMTP nu poate fi gol. + smtp_password: + label: Parola SMTP + msg: Parola SMTP nu poate fi goală. + test_email_recipient: + label: Destinatari de e-mail test + text: Furnizați adresa de e-mail care va primi trimiterile de teste. + msg: Destinatarii de e-mail de test sunt invalizi + smtp_authentication: + label: Activare Authenticator + title: Autentificare SMTP + msg: Autentificarea SMTP nu poate fi goală. + "yes": "Da" + "no": "Nu" + branding: + page_title: Marcă + logo: + label: Logo + msg: Logo-ul nu poate fi gol. + text: Imaginea logo-ului din stânga sus a site-ului dvs. Utilizaţi o imagine dreptunghiulară largă cu o înălţime de 56 şi un raport de aspect mai mare de 3:1. Dacă nu se completează, textul pentru titlul site-ului va fi afișat. + mobile_logo: + label: Logo mobil + text: Logo-ul folosit pe versiunea mobila a site-ului dvs. Utilizați o imagine dreptunghiulară largă cu o înălțime de 56. Dacă nu o completați, va fi utilizată imaginea din setarea "logo". + square_icon: + label: Pictogramă pătrată + msg: Pictograma pătrată nu poate fi goală. + text: Imaginea folosită ca bază pentru pictogramele de metadate. Ar trebui să fie, în mod ideal, mai mare de 512x512. + favicon: + label: Favicon + text: O pictogramă favorită pentru site-ul dvs. Pentru a lucra corect peste un CDN trebuie să fie un png. Va fi redimensionată la 32x32. Dacă este lăsat necompletat, va fi folosită "iconiță pătrată". + legal: + page_title: Juridic + terms_of_service: + label: Termeni și condiții + text: "Puteți adăuga aici termeni de conținut pentru serviciu. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici." + privacy_policy: + label: Politică de confidențialitate + text: "Puteți adăuga aici termeni politicii de confidențialitate. Dacă aveți deja un document găzduit în altă parte, furnizați URL-ul complet aici." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Scrie + restrict_answer: + title: Answer write + label: Fiecare utilizator poate scrie doar câte un răspuns pentru fiecare întrebare + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Etichete recomandate + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Fiecare întrebare nouă trebuie să aibă cel puțin o etichetă recomandată." + reserved_tags: + label: Etichete rezervate + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Legătură permanenta + text: Structurile URL personalizate pot îmbunătăți capacitatea de utilizare și compatibilitatea link-urilor tale. + robots: + label: robots.txt + text: Acest lucru va suprascrie permanent orice setări ale site-ului. + themes: + page_title: Teme + themes: + label: Teme + text: Selectaţi o temă existentă. + color_scheme: + label: Paletă de culori + navbar_style: + label: Navbar background style + primary_color: + label: Culoare primară + text: Modifică culorile folosite de temele tale + css_and_html: + page_title: CSS și HTML + custom_css: + label: CSS personalizat + text: > + + head: + label: Cap + text: > + + header: + label: Antet + text: > + + footer: + label: Subsol + text: Se va insera înainte de </body>. + sidebar: + label: Bară laterală + text: Acesta va fi inserat în bara laterală. + login: + page_title: Autentifică-te + membership: + title: Calitatea de membru + label: Permite înregistrări noi + text: Dezactivați pentru a împiedica pe oricine să creeze un cont nou. + email_registration: + title: Înregistrare e-mail + label: Permite înregistrări noi + text: Dezactivați pentru a preveni crearea unui cont nou prin e-mail. + allowed_email_domains: + title: Domenii de e-mail permise + text: Domeniile de e-mail cu care utilizatorii trebuie să înregistreze conturi. Un domeniu pe linie. Se ignoră atunci când este gol. + private: + title: Privat + label: Autentificare necesară + text: Numai utilizatorii autentificați pot accesa această comunitate. + password_login: + title: Parola de login + label: Permiteți autentificarea prin e-mail și parolă + text: "AVERTISMENT: Dacă opriți, este posibil să nu vă puteți conecta dacă nu ați configurat anterior o altă metodă de autentificare." + installed_plugins: + title: Extensii instalate + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: Toate + active: Activ + inactive: Inactiv + outdated: Învechit + plugins: + label: Extensii + text: Selectați un plugin existent. + name: Nume + version: Versiune + status: Stare + action: Acţiune + deactivate: Dezactivare + activate: Activare + settings: Setări + settings_users: + title: Utilizatori + avatar: + label: Avatarul implicit + text: Pentru utilizatorii fără un avatar personalizat propriu. + gravatar_base_url: + label: URL de bază Gravatar + text: URL-ul bazei API a furnizorului de Gravatar. Ignorat când este gol. + profile_editable: + title: Profil editabil + allow_update_display_name: + label: Permite utilizatorilor să își schimbe numele afișat + allow_update_username: + label: Permite utilizatorilor să își schimbe numele de utilizator + allow_update_avatar: + label: Permite utilizatorilor să își schimbe imaginea de profil + allow_update_bio: + label: Permite utilizatorilor să își schimbe propriul lor despre mine + allow_update_website: + label: Permite utilizatorilor să îşi schimbe website-ul + allow_update_location: + label: Permite utilizatorilor să își schimbe locația + privilege: + title: Privilegii + level: + label: Nivel necesar de reputație + text: Alegeți reputația necesară pentru privilegii + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (opțional) + empty: nu poate fi lăsat necompletat + invalid: nu este valid + btn_submit: Salvează + not_found_props: "Proprietatea solicitată {{ key }} nu a fost găsită." + select: Selectează + page_review: + review: Recenzie + proposed: propus + question_edit: Editare întrebări + answer_edit: Editările răspunsului + tag_edit: Editare etichetă + edit_summary: Editează sumarul + edit_question: Editați întrebarea + edit_answer: Editați răspunsul + edit_tag: Editare etichetă + empty: Nu au mai rămas sarcini de evaluare. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Tip + reputation: reputation + flag_post_type: Acest post a fost marcat de {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Editează postarea + list_post: Listează postarea + unlist_post: Unlist post + timeline: + undeleted: restabilește + deleted: şterse + downvote: vot negativ + upvote: vot pozitiv + accept: acceptat + cancelled: anulat + commented: comentat + rollback: revenire + edited: editat + answered: răspunse + asked: întrebat + closed: închise + reopened: redeschise + created: creat + pin: fixat + unpin: nefixat + show: listă + hide: nelistat + title: "Istoric pentru" + tag_title: "Cronologie pentru" + show_votes: "Afișare voturi" + n_or_a: N/A + title_for_question: "Cronologie pentru" + title_for_answer: "Calendarul răspunsului la {{ title }} cu {{ author }}" + title_for_tag: "Cronologie pentru etichetă" + datetime: Dată și oră + type: Tip + by: De către + comment: Comentariu + no_data: "Nu a fost găsit nimic." + users: + title: Utilizatori + users_with_the_most_reputation: Utilizatori cu cele mai mari scoruri ale reputaţiei în această săptămână + users_with_the_most_vote: Utilizatorii care au votat cel mai mult săptămâna aceasta + staffs: Personalul acestei comunități + reputation: reputație + votes: voturi + prompt: + leave_page: Sunteți sigur că doriți să ieșiți din pagina asta? + changes_not_save: Modificările nu pot fi salvate. + draft: + discard_confirm: Ești sigur că vrei să renunți la ciornă? + messages: + post_deleted: Această postare a fost ștearsă. + post_cancel_deleted: This post has been undeleted. + post_pin: Această postare a fost fixată. + post_unpin: Această postare nu a fost fixată. + post_hide_list: Această postare a fost ascunsă din listă. + post_show_list: Această postare a fost afișată în listă. + post_reopen: Această postare a fost redeschisă. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/ru_RU.yaml b/i18n/ru_RU.yaml new file mode 100644 index 000000000..c71a27e37 --- /dev/null +++ b/i18n/ru_RU.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Выполнено. + unknown: + other: Неизвестная ошибка. + request_format_error: + other: Формат файла не корректен. + unauthorized_error: + other: Авторизация не выполнена. + database_error: + other: Ошибка сервера данных. + forbidden_error: + other: Доступ запрещен. + duplicate_request_error: + other: Дублирующая отправка. + action: + report: + other: Пожаловаться + edit: + other: Редактировать + delete: + other: Удалить + close: + other: Закрыть + reopen: + other: Открыть + forbidden_error: + other: Доступ запрещен. + pin: + other: Закрепить + hide: + other: Убрать + unpin: + other: Открепить + show: + other: Список + invite_someone_to_answer: + other: Редактировать + undelete: + other: Отменить удаление + merge: + other: Merge + role: + name: + user: + other: Пользователь + admin: + other: Администратор + moderator: + other: Модератор + description: + user: + other: По умолчанию, без специального доступа. + admin: + other: Имейте все полномочия для доступа к сайту. + moderator: + other: Имеет доступ ко всем сообщениям, кроме настроек администратора. + privilege: + level_1: + description: + other: Уровень 1 (для приватной команды, группы требуется наименьшая репутация) + level_2: + description: + other: Уровень 2 (для стартапа достаточен низкий уровень репутации) + level_3: + description: + other: Уровень 3 (для зрелого сообщества требуется высокая репутация) + level_custom: + description: + other: Настраиваемый уровень + rank_question_add_label: + other: Задать вопрос + rank_answer_add_label: + other: Написать ответ + rank_comment_add_label: + other: Написать комментарий + rank_report_add_label: + other: Пожаловаться + rank_comment_vote_up_label: + other: Полезный комментарий + rank_link_url_limit_label: + other: Опубликовать более 2 ссылок за раз + rank_question_vote_up_label: + other: Полезный вопрос + rank_answer_vote_up_label: + other: Полезный ответ + rank_question_vote_down_label: + other: Бесполезный вопрос + rank_answer_vote_down_label: + other: Бесполезный ответ + rank_invite_someone_to_answer_label: + other: Пригласить кого-нибудь ответить + rank_tag_add_label: + other: Новый тег + rank_tag_edit_label: + other: Редактировать описание тега (требуется проверка) + rank_question_edit_label: + other: Редактировать вопрос другого пользователя (требуется проверка) + rank_answer_edit_label: + other: Редактировать ответ другого пользователя (требуется проверка) + rank_question_edit_without_review_label: + other: Редактировать вопрос другого пользователя без проверки + rank_answer_edit_without_review_label: + other: Редактировать ответ другого пользователя без проверки + rank_question_audit_label: + other: Проверить изменения вопроса + rank_answer_audit_label: + other: Проверить изменения ответа + rank_tag_audit_label: + other: Проверить изменения тегов + rank_tag_edit_without_review_label: + other: Редактировать описание тега без проверки + rank_tag_synonym_label: + other: Управлять синонимами тегов + email: + other: Эл. почта + e_mail: + other: Почта + password: + other: Пароль + pass: + other: Пароль + old_pass: + other: Current password + original_text: + other: Это сообщение + email_or_password_wrong_error: + other: Неверное имя пользователя или пароль. + error: + common: + invalid_url: + other: Неверная URL. + status_invalid: + other: Неверный статус. + password: + space_invalid: + other: Пароль не должен содержать пробелы. + admin: + cannot_update_their_password: + other: Вы не можете изменить свой пароль. + cannot_edit_their_profile: + other: Вы не можете изменять свой профиль. + cannot_modify_self_status: + other: Вы не можете изменить свой статус. + email_or_password_wrong: + other: Неверное имя пользователя или пароль. + answer: + not_found: + other: Ответ не найден. + cannot_deleted: + other: Недостаточно прав для удаления. + cannot_update: + other: Нет прав для обновления. + question_closed_cannot_add: + other: Вопросы закрыты и не могут быть добавлены. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Комментарий не может редактироваться. + not_found: + other: Комментарий не найден. + cannot_edit_after_deadline: + other: Невозможно редактировать комментарий из-за того, что он был создан слишком давно. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Адрес электронной почты уже существует. + need_to_be_verified: + other: Адрес электронной почты должен быть подтвержден. + verify_url_expired: + other: Срок действия подтверждённого адреса электронной почты истек, пожалуйста, отправьте письмо повторно. + illegal_email_domain_error: + other: Невозможно использовать email с этим доменом. Пожалуйста, используйте другой. + lang: + not_found: + other: Языковой файл не найден. + object: + captcha_verification_failed: + other: Captcha введена неверно. + disallow_follow: + other: Вы не можете подписаться. + disallow_vote: + other: Вы не можете голосовать. + disallow_vote_your_self: + other: Вы не можете голосовать за собственный отзыв. + not_found: + other: Объект не найден. + verification_failed: + other: Проверка не удалась. + email_or_password_incorrect: + other: Email или пароль не совпадают. + old_password_verification_failed: + other: Не удалось подтвердить старый пароль + new_password_same_as_previous_setting: + other: Пароль не может быть таким же как прежний. + already_deleted: + other: Этот пост был удален. + meta: + object_not_found: + other: Объект мета не найден + question: + already_deleted: + other: Этот пост был удалён. + under_review: + other: Ваш пост ожидает проверки. Он станет видимым после одобрения. + not_found: + other: Вопрос не найден. + cannot_deleted: + other: Недостаточно прав для удаления. + cannot_close: + other: Нет разрешения на закрытие. + cannot_update: + other: Нет разрешения на обновление. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Ранг репутации не соответствует условию. + vote_fail_to_meet_the_condition: + other: Спасибо за отзыв. Вам нужно как минимум {{.Rank}} репутация для голосования. + no_enough_rank_to_operate: + other: Для этого вам нужна репутация {{.Rank}}. + report: + handle_failed: + other: Не удалось обработать отчет. + not_found: + other: Отчет не найден. + tag: + already_exist: + other: Тег уже существует. + not_found: + other: Тег не найден. + recommend_tag_not_found: + other: Рекомендуемый тег не существует. + recommend_tag_enter: + other: Пожалуйста, введите хотя бы один тег. + not_contain_synonym_tags: + other: Не должно содержать теги синонимы. + cannot_update: + other: Нет прав для обновления. + is_used_cannot_delete: + other: Вы не можете удалить метку, которая используется. + cannot_set_synonym_as_itself: + other: Вы не можете установить синоним текущего тега. + smtp: + config_from_name_cannot_be_email: + other: Поле отправителя не может содержать email адрес. + theme: + not_found: + other: Тема не найдена. + revision: + review_underway: + other: В настоящее время не удается редактировать версию, в очереди на проверку. + no_permission: + other: Разрешения на пересмотр нет. + user: + external_login_missing_user_id: + other: Сторонняя платформа не предоставляет уникальный идентификатор пользователя, поэтому вы не можете войти в систему, пожалуйста, свяжитесь с администратором веб-сайта. + external_login_unbinding_forbidden: + other: Пожалуйста, установите пароль для входа в свою учетную запись, прежде чем удалять этот логин. + email_or_password_wrong: + other: + other: Почта и пароль введены неправильно. + not_found: + other: Пользователь не найден. + suspended: + other: Пользователь был заблокирован. + username_invalid: + other: Недопустимое имя пользователя. + username_duplicate: + other: Имя пользователя уже используется. + set_avatar: + other: Не удалось установить аватар. + cannot_update_your_role: + other: Вы не можете изменить свою роль. + not_allowed_registration: + other: В данный момент регистрация на сайте выключена. + not_allowed_login_via_password: + other: В настоящее время вход на сайт по паролю отключен. + access_denied: + other: Доступ запрещен + page_access_denied: + other: У вас нет доступа к этой странице. + add_bulk_users_format_error: + other: "Ошибка формата {{.Field}} рядом с '{{.Content}}' в строке {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Количество пользователей, которое Вы добавляете, должно быть в промежутке от 1 до {{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Не удалось прочитать конфигурацию + database: + connection_failed: + other: Ошибка подключения к базе данных + create_table_failed: + other: Не удалось создать таблицу + install: + create_config_failed: + other: Не удалось создать файл config.yaml. + upload: + unsupported_file_format: + other: Неподдерживаемый формат файла. + site_info: + config_not_found: + other: Конфигурация сайта не найдена. + badge: + object_not_found: + other: Объект бейджа не найден + reason: + spam: + name: + other: Спам + desc: + other: Этот пост является рекламой или вандализмом. Он не полезен и не имеет отношения к текущей теме. + rude_or_abusive: + name: + other: Грубость или оскорбления + desc: + other: "Человек может посчитать такое содержимое неподходящим для уважительной беседы." + a_duplicate: + name: + other: дубликат + desc: + other: Этот вопрос уже был задан, и на него уже был получен ответ. + placeholder: + other: Введите существующую ссылку на вопрос + not_a_answer: + name: + other: это не ответ + desc: + other: "Это сообщение было опубликовано в качестве ответа, но оно не пытается ответить на вопрос. Возможно, оно должно быть отредактировано, дополнено, быть другим вопросом или удалено навсегда." + no_longer_needed: + name: + other: Не актуально + desc: + other: Этот комментарий устарел, носит разговорный характер или не имеет отношения к данному сообщению. + something: + name: + other: Прочее + desc: + other: Этот пост требует внимания администрации по другой причине, не перечисленной выше. + placeholder: + other: Уточните, что именно Вас беспокоит + community_specific: + name: + other: специфическая для сообщества причина + desc: + other: Этот вопрос не соответствует рекомендациям сообщества. + not_clarity: + name: + other: нуждается в деталях или ясности + desc: + other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме. + looks_ok: + name: + other: выглядит нормально + desc: + other: Этот пост хороший и достойного качества. + needs_edit: + name: + other: нуждается в редактировании, и я сделал это + desc: + other: Устраните проблемы с этим сообщением самостоятельно. + needs_close: + name: + other: требует закрытия + desc: + other: На закрытый вопрос нельзя ответить, но все равно можно редактировать, голосовать и комментировать. + needs_delete: + name: + other: требует удаления + desc: + other: Этот пост будет удален. + question: + close: + duplicate: + name: + other: спам + desc: + other: Этот вопрос был задан ранее и уже имеет ответ. + guideline: + name: + other: специфическая для сообщества причина + desc: + other: Этот вопрос не соответствует рекомендациям сообщества. + multiple: + name: + other: нуждается в деталях или ясности + desc: + other: В настоящее время этот вопрос включает в себя несколько вопросов в одном. Он должен быть сосредоточен только на одной проблеме. + other: + name: + other: прочее + desc: + other: Для этого поста требуется другая причина, не указанная выше. + operation_type: + asked: + other: вопросы + answered: + other: отвеченные + modified: + other: измененные + deleted_title: + other: Удаленные вопросы + questions_title: + other: Вопросы + tag: + tags_title: + other: Теги + no_description: + other: Тег не имеет описания. + notification: + action: + update_question: + other: обновленные вопросы + answer_the_question: + other: отвеченные вопросы + update_answer: + other: обновленные ответы + accept_answer: + other: принятые ответы + comment_question: + other: Прокомментированные ответы + comment_answer: + other: прокоментированные ответы + reply_to_you: + other: отвеченные вам + mention_you: + other: с упоминанием вас + your_question_is_closed: + other: Ваш вопрос был закрыт + your_question_was_deleted: + other: Ваш вопрос был удален + your_answer_was_deleted: + other: Ваш ответ был удален + your_comment_was_deleted: + other: Ваш комментарий был удален + up_voted_question: + other: поддержанный вопрос + down_voted_question: + other: неподдержанный вопрос + up_voted_answer: + other: ответ "за" + down_voted_answer: + other: ответ "против" + up_voted_comment: + other: поддержанный комментарий + invited_you_to_answer: + other: пригласил вас ответить + earned_badge: + other: Вы заработали значок "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Подтвердите новый адрес электронной почты" + body: + other: "Подтвердите свой новый адрес электронной почты для {{.SiteName}}, перейдя по следующей ссылке:
{{.ChangeEmailUrl}}

Если вы не запрашивали это изменение, пожалуйста, проигнорируйте это электронное письмо.

-
Примечание: Данное сообщение является автоматическим, отвечать на него не нужно." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} ответил на ваш вопрос" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться." + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} приглашает вас в Answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Я думаю, что вы можете знать ответ.

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} прокомментировал под вашей публикацией" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nОткрыть {{.SiteName}}

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно.

\n\nОтписаться" + new_question: + title: + other: "[{{.SiteName}}] Новый вопрос: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Пароль сброшен" + body: + other: "Кто-то попросил сбросить ваш пароль на {{.SiteName}}.

\n\nЕсли это не вы, вы можете проигнорировать это письмо.

\n\nПерейдите по следующей ссылке, чтобы выбрать новый пароль:
\n{{.PassResetUrl}}\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." + register: + title: + other: "[{{.SiteName}}] Подтвердите Ваш новый аккаунт" + body: + other: "Добро пожаловать в {{.SiteName}}!

\n\nПерейдите по следующей ссылке для подтверждения и активации вашей новой учетной записи:
\n{{.RegisterUrl}}

\n\nЕсли ссылка выше не нажата, попробуйте скопировать и вставить её в адресную строку вашего браузера.\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." + test: + title: + other: "[{{.SiteName}}] Проверочное электронное письмо" + body: + other: "Это тестовое сообщение.\n

\n\n--
\nПримечание: Данное сообщение является автоматическим, отвечать на него не нужно." + action_activity_type: + upvote: + other: проголосовать за + upvoted: + other: проголосовано за + downvote: + other: бесполезный + downvoted: + other: проголосовано против + accept: + other: принять + accepted: + other: принято + edit: + other: редактировать + review: + queued_post: + other: Задан вопрос + flagged_post: + other: Отмеченный пост + suggested_post_edit: + other: Предложенные исправления + reaction: + tooltip: + other: "{{ .Names }} и {{ .Count }} еще..." + badge: + default_badges: + autobiographer: + name: + other: Автобиограф + desc: + other: Заполнена информация об профиле. + certified: + name: + other: Сертифицированный + desc: + other: Завершил наше новое руководство пользователя. + editor: + name: + other: Редактор + desc: + other: Впервые отредактировать сообщение + first_flag: + name: + other: Первый флаг + desc: + other: Впервые проставить флаг в сообщения + first_upvote: + name: + other: Первый голос + desc: + other: Впервые добавить голос в сообщении. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Оставить 5 комментариев. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Прочтите [правила сообщества]. + reader: + name: + other: Читатель + desc: + other: Прочитать каждый ответ в разделе с более чем 10 ответами. + welcome: + name: + other: Добро пожаловать + desc: + other: Получен голос «за». + nice_share: + name: + other: Неплохо поделился + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Спасибо + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 'Форматирование:' + desc: >- + + pagination: + prev: Назад + next: Следующий + page_title: + question: Вопрос + questions: Вопросы + tag: Тэг + tags: Теги + tag_wiki: wiki тэг + create_tag: Создать тег + edit_tag: Изменить тег + ask_a_question: Create Question + edit_question: Редактировать вопрос + edit_answer: Редактировать ответ + search: Поиск + posts_containing: Посты содержащие + settings: Настройки + notifications: Уведомления + login: Вход + sign_up: Регистрация + account_recovery: Восстановление аккаунта + account_activation: Активация учётной записи + confirm_email: Подтвердить адрес электронной почты + account_suspended: Аккаунт заблокирован + admin: Управление + change_email: Изменить Email + install: Установка ответа + upgrade: Обновить ответ + maintenance: Обслуживание сайта + users: Пользователи + oauth_callback: Идет обработка + http_404: Ошибка HTTP 404 + http_50X: Ошибка HTTP 500 + http_403: Ошибка HTTP 403 + logout: Выйти + notifications: + title: Уведомления + inbox: Входящие + achievement: Достижения + new_alerts: Новые оповещения + all_read: Отметить всё как прочитанное + show_more: Показать еще + someone: Кто-то + inbox_type: + all: Все + posts: Посты + invites: Приглашения + votes: Голоса + answer: Ответ + question: Вопрос + badge_award: Значок + suspended: + title: Ваш аккаунт заблокирован + until_time: "Ваша учетная запись была заблокирована до {{ time }}." + forever: Этот пользователь был навсегда заблокирован. + end: Вы не соответствуете правилам сообщества. + contact_us: Связаться с нами + editor: + blockquote: + text: Цитата + bold: + text: Сильный + chart: + text: Диаграмма + flow_chart: Блок-схема + sequence_diagram: Диаграмма последовательности + class_diagram: Диаграмма классов + state_diagram: Диаграмма состояний + entity_relationship_diagram: Диаграмма связей сущностей + user_defined_diagram: Пользовательская диаграмма + gantt_chart: Диаграмма Гантта + pie_chart: Круговая диаграмма + code: + text: Фрагмент кода + add_code: Добавить пример кода + form: + fields: + code: + label: Код + msg: + empty: Код не может быть пустым. + language: + label: Язык + placeholder: Автоматический выбор + btn_cancel: Отменить + btn_confirm: Добавить + formula: + text: Формула + options: + inline: Встроенная формула + block: Блочная формула + heading: + text: Заголовок + options: + h1: Заголовок 1 + h2: Заголовок 2 + h3: Заголовок 3 + h4: Заголовок 4 + h5: Заголовок 5 + h6: Заголовок 6 + help: + text: Помощь + hr: + text: Горизонтальная линия + image: + text: Изображение + add_image: Добавить изображение + tab_image: Загрузить изображение + form_image: + fields: + file: + label: Файл изображения + btn: Выбрать изображение + msg: + empty: Файл не может быть пустым. + only_image: Разрешены только изображения. + max_size: Размер файла не может превышать {{size}} МБ. + desc: + label: Описание + tab_url: URL изображения + form_url: + fields: + url: + label: URL изображения + msg: + empty: URL изображения не может быть пустым. + name: + label: Описание + btn_cancel: Отменить + btn_confirm: Добавь + uploading: Загрузка + indent: + text: Абзац + outdent: + text: Уменьшить отступ + italic: + text: Курсив + link: + text: Гиперссылка + add_link: Вставить гиперссылку + form: + fields: + url: + label: URL-адрес + msg: + empty: URL не может быть пустым. + name: + label: Описание + btn_cancel: Отменить + btn_confirm: Добавить + ordered_list: + text: Нумерованный список + unordered_list: + text: Маркированный список + table: + text: Таблица + heading: Заголовок + cell: Ячейка + file: + text: Прикрепить файлы + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Я закрываю этот пост как... + btn_cancel: Отменить + btn_submit: Сохранить + remark: + empty: Не может быть пустым. + msg: + empty: Пожалуйста, выбери причину. + report_modal: + flag_title: 'Причина жалобы:' + close_title: Я закрываю этот пост как... + review_question_title: Проверить вопрос + review_answer_title: Проверить ответ + review_comment_title: Просмотр комментариев + btn_cancel: Отмена + btn_submit: Сохранить + remark: + empty: Не может быть пустым. + msg: + empty: Пожалуйста, выбери причину. + not_a_url: Недопустимый формат URL. + url_not_match: URL адрес не соответствует текущему веб-сайту. + tag_modal: + title: Новый тег + form: + fields: + display_name: + label: Отображаемое имя + msg: + empty: Отображаемое название не может быть пустым. + range: Отображаемое имя до 35 символов. + slug_name: + label: URL-адрес тега + desc: URL-адрес тега длиной до 35 символов. + msg: + empty: URL slug не может быть пустым. + range: URL slug до 35 символов. + character: URL slug содержит недопустимый набор символов. + desc: + label: Описание + revision: + label: Версия + edit_summary: + label: Отредактировать сводку + placeholder: >- + Коротко опишите изменения (орфография, грамматики, улучшение формата) + btn_cancel: Отмена + btn_submit: Сохрнаить + btn_post: Создать новый тег + tag_info: + created_at: Создано + edited_at: Отредактировано + history: История + synonyms: + title: Синонимы + text: Следующие теги будут переназначены на + empty: Синонимы не найдены. + btn_add: Добавить синоним + btn_edit: Редактировать + btn_save: Сохранить + synonyms_text: Следующие теги будут переназначены на + delete: + title: Удалить этот тег + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Вы уверены, что хотите удалить? + close: Закрыть + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Изменить тег + default_reason: Правка тега + default_first_reason: Добавить метку + btn_save_edits: Сохранить изменения + btn_cancel: Отмена + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: сейчас + x_seconds_ago: "{{count}}с назад" + x_minutes_ago: "{{count}}м назад" + x_hours_ago: "{{count}}ч назад" + hour: часы + day: дней + hours: часов + days: дней + month: month + months: months + year: year + reaction: + heart: сердечко + smile: smile + frown: frown + btn_label: добавить или удалить реакции + undo_emoji: отменить реакцию {{ emoji }} + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Добавить комментарий + reply_to: Ответить на + btn_reply: Ответить + btn_edit: Редактирование + btn_delete: Удалить + btn_flag: Пожаловаться + btn_save_edits: Сохранить изменения + btn_cancel: Отменить + show_more: "Еще {{count}} комментарий" + tip_question: >- + Воспользуйтесь комментариями, чтобы запросить больше информации или предложить улучшения. Не отвечайте на вопросы в комментариях. + tip_answer: >- + Используйте комментарии для ответа другим пользователям или уведомления об изменениях. Если вы добавляете новую информацию, редактируйте ваше сообщение вместо комментариев. + tip_vote: Это добавляет кое-что полезное к сообщению + edit_answer: + title: Редактировать ответ + default_reason: Редактировать ответ + default_first_reason: Добавить ответ + form: + fields: + revision: + label: Пересмотр + answer: + label: Ответ + feedback: + characters: длина пароля должна составлять не менее 6 символов. + edit_summary: + label: Изменить краткое описание + placeholder: >- + Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование) + btn_save_edits: Сохранить изменения + btn_cancel: Отменить + tags: + title: Теги + sort_buttons: + popular: Популярное + name: Имя + newest: Последние + button_follow: Подписаться + button_following: Подписки + tag_label: вопросы + search_placeholder: Фильтр по названию тега + no_desc: Тег не имеет описания. + more: Подробнее + wiki: Wiki + ask: + title: Create Question + edit_title: Редактировать вопрос + default_reason: Редактировать вопрос + default_first_reason: Create question + similar_questions: Похожие вопросы + form: + fields: + revision: + label: Версия + title: + label: Заголовок + placeholder: What's your topic? Be specific. + msg: + empty: Заголовок не может быть пустым. + range: Заголовок должен быть меньше 150 символов + body: + label: 'Вопрос:' + msg: + empty: Вопрос не может быть пустым. + tags: + label: Теги + msg: + empty: Теги не могут быть пустыми. + answer: + label: Ответ + msg: + empty: Ответ не может быть пустым. + edit_summary: + label: Изменить краткое описание + placeholder: >- + Кратко опишите вносимые изменения (исправлена орфография, исправлена грамматика, улучшено форматирование) + btn_post_question: Задать вопрос + btn_save_edits: Сохранить изменения + answer_question: Ответить на свой собственный вопрос + post_question&answer: Опубликуйте свой вопрос и ответ + tag_selector: + add_btn: Тег + create_btn: новый тег + search_tag: Поиск тега + hint: "Describe what your content is about, at least one tag is required." + no_result: Нет соответствующих тэгов + tag_required_text: Обязательный тег (хотя бы один) + header: + nav: + question: Вопросы + tag: Теги + user: Пользователи + badges: Значки + profile: Профиль + setting: Настройки + logout: Выйти + admin: Управление + review: Рецензия + bookmark: Закладки + moderation: Модерирование + search: + placeholder: Поиск + footer: + build_on: >- + Работает на <1> Apache Answer - программном обеспечении с открытым исходным кодом, которое поддерживает сообщества вопросов и ответов.
Сделано с любовью © {{cc}}. + upload_img: + name: Изменить + loading: загрузка... + pic_auth_code: + title: Капча + placeholder: Введите текст выше + msg: + empty: Капча не может быть пустой. + inactive: + first: >- + Вы почти закончили! Мы отправили письмо с активацией на адрес {{mail}}. Пожалуйста, следуйте инструкциям в письме, чтобы активировать свою учетную запись. + info: "Если оно не пришло, проверьте свою папку со спамом." + another: >- + Мы отправили вам еще одно электронное письмо с активацией по адресу {{mail}}. Его получение может занять несколько минут; обязательно проверьте папку со спамом. + btn_name: Повторно отправить письмо с активацией + change_btn_name: Изменить email + msg: + empty: Не может быть пустым. + resend_email: + url_label: Вы уверены, что хотите повторно отправить письмо с активацией? + url_text: Вы также можете предоставить пользователю ссылку для активации выше. + login: + login_to_continue: Войдите, чтобы продолжить + info_sign: У вас нет аккаунта? <1>Зарегистрируйтесь + info_login: Уже есть аккаунт? <1>Войти + agreements: Регистрируясь, вы соглашаетесь с <1>политикой конфиденциальности и <3>условиями обслуживания. + forgot_pass: Забыли пароль? + name: + label: Имя пользователя + msg: + empty: Имя пользователя не должно быть пустым. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email адрес + msg: + empty: Адрес электронной почты не может быть пустым. + password: + label: Пароль + msg: + empty: Пароль не может быть пустым. + different: Введенные пароли не совпадают + account_forgot: + page_title: Забыли свой пароль + btn_name: Отправить мне письмо для восстановления пароля + send_success: >- + Если учетная запись соответствует {{mail}}, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль. + email: + label: Email адрес + msg: + empty: Адрес электронной почты не может быть пустым. + change_email: + btn_cancel: Отмена + btn_update: Сменить адрес email + send_success: >- + Если учетная запись соответствует {{mail}}, вы должны в ближайшее время получить электронное письмо с инструкциями о том, как сбросить пароль. + email: + label: Новый email + msg: + empty: Email не может быть пустым. + oauth: + connect: Связаться с {{ auth_name }} + remove: Удалить {{ auth_name }} + oauth_bind_email: + subtitle: Добавьте адрес электронной почты для восстановления учетной записи. + btn_update: Сменить адрес email + email: + label: Электронная почта + msg: + empty: Адрес электронной почты не может быть пустым. + modal_title: Электронная почта уже существует. + modal_content: Этот адрес электронной почты уже зарегистрирован. Вы уверены, что хотите подключиться к существующей учетной записи? + modal_cancel: Изменить адрес электронной почты + modal_confirm: Подключение к существующей учетной записи + password_reset: + page_title: Сброс пароля + btn_name: Сбросить мой пароль + reset_success: >- + Вы успешно сменили свой пароль; вы будете перенаправлены на страницу входа в систему. + link_invalid: >- + Извините, эта ссылка для сброса пароля больше недействительна. Возможно, ваш пароль уже сброшен? + to_login: Перейдите на страницу входа в систему + password: + label: Пароль + msg: + empty: Пароль не может быть пустым. + length: Длина должна быть от 8 до 32 + different: Введенные пароли не совпадают + password_confirm: + label: Подтвердите новый пароль + settings: + page_title: Настройки + goto_modify: Перейдите к изменению + nav: + profile: Профиль + notification: Уведомления + account: Учетная запись + interface: Интерфейс + profile: + heading: Профиль + btn_name: Сохранить + display_name: + label: Отображаемое имя + msg: Отображаемое имя не может быть пустым. + msg_range: Display name must be 2-30 characters in length. + username: + label: Имя пользователя + caption: Люди могут упоминать вас как "@username". + msg: Имя пользователя не может быть пустым. + msg_range: Username must be 2-30 characters in length. + character: 'Необходимо использовать набор символов "a-z", "0-9", " - . _"' + avatar: + label: Изображение профиля + gravatar: Gravatar + gravatar_text: Вы можете изменить изображение на + custom: Другой + custom_text: Вы можете загрузить свое изображение. + default: Системные + msg: Пожалуйста, загрузите аватар + bio: + label: Обо мне + website: + label: Сайт + placeholder: "https://example.com" + msg: Неправильный формат веб-сайта + location: + label: Местоположение + placeholder: "Город, страна" + notification: + heading: Уведомления по эл. почте + turn_on: Вкл. + inbox: + label: Email уведомления + description: Ответы на ваши вопросы, комментарии, приглашения и многое другое. + all_new_question: + label: Все новые вопросы + description: Получайте уведомления обо всех новых вопросах. До 50 вопросов в неделю. + all_new_question_for_following_tags: + label: Все новые вопросы для тегов из подписок + description: Получайте уведомления о новых вопросах по следующим тегам. + account: + heading: Учетная запись + change_email_btn: Изменить e-mail + change_pass_btn: Изменить пароль + change_email_info: >- + Мы отправили электронное письмо на этот адрес. Пожалуйста, следуйте инструкциям из письма. + email: + label: Email + new_email: + label: Новый email + msg: Новый email не может быть пустым. + pass: + label: Текущий пароль + msg: Пароль не может быть пустым. + password_title: Пароль + current_pass: + label: Текущий пароль + msg: + empty: Текущий пароль не может быть пустым. + length: Длина должна быть от 8 до 32. + different: Введенные пароли не совпадают. + new_pass: + label: Новый пароль + pass_confirm: + label: Подтвердите новый пароль + interface: + heading: Интерфейс + lang: + label: Язык интерфейса + text: Язык пользовательского интерфейса. Он изменится при обновлении страницы. + my_logins: + title: Мои логины + label: Войдите в систему или зарегистрируйтесь на этом сайте, используя эти учетные записи. + modal_title: Удаление логина + modal_content: Вы уверены, что хотите удалить этот логин из своей учетной записи? + modal_confirm_btn: Удалить + remove_success: Успешно удалено + toast: + update: успешное обновление + update_password: Пароль успешно изменен. + flag_success: Благодарим за отметку. + forbidden_operate_self: Запрещено работать с собой + review: Ваша версия будет отображаться после проверки. + sent_success: Отправлено успешно + related_question: + title: Related + answers: ответы + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Позвать на помощь + desc: Выберите людей, которые, по вашему мнению, могут знать ответ. + invite: Пригласил вас ответить + add: Добавить пользователей + search: Поиск людей + question_detail: + action: Действия + Asked: Спросил(а) + asked: спросил(а) + update: Изменён + edit: отредактировал + commented: commented + Views: Просмотрен + Follow: Подписаться + Following: Подписки + follow_tip: Подпишитесь на этот вопрос для получения уведомлений + answered: отвеченные + closed_in: Закрыто в + show_exist: Показать существующий вопрос. + useful: Полезный + question_useful: Это полезно и понятно + question_un_useful: Это непонятно или не полезно + question_bookmark: Добавьте этот вопрос в закладки + answer_useful: Это полезно + answer_un_useful: Это бесполезно + answers: + title: Ответы + score: Оценка + newest: Последние + oldest: Oldest + btn_accept: Принять + btn_accepted: Принято + write_answer: + title: Ваш ответ + edit_answer: Редактировать мой существующий ответ + btn_name: Ответить + add_another_answer: Добавить другой ответ + confirm_title: Перейти к ответу + continue: Продолжить + confirm_info: >- +

Вы уверены, что хотите добавить другой ответ?

Вы можете использовать ссылку редактирования для уточнения и улучшения существующего ответа.

+ empty: Ответ не может быть пустым. + characters: длина содержимого должна составлять не менее 6 символов. + tips: + header_1: Спасибо за ответ + li1_1: Пожалуйста, обязательно отвечайте на вопрос. Предоставьте подробности и поделитесь результатами своих исследований. + li1_2: Поддерживайте свои высказывания ссылками или личным опытом. + header_2: Но избегайте ... + li2_1: Просить о помощи, запрашивать уточнения или отвечать на другие ответы. + reopen: + confirm_btn: Снова открыть + title: Открыть повторно этот пост + content: Вы уверены, что хотите открыть заново? + list: + confirm_btn: Список + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Убрать из списка + title: + content: Are you sure you want to unlist? + pin: + title: Закрепить сообщение + content: Вы уверены, что хотите закрепить глобально? Это сообщение появится вверху всех списков сообщений. + confirm_btn: Закрепить + delete: + title: Удалить сообщение + question: >- + Мы не рекомендуем удалять вопросы с ответами, поскольку это лишает будущих читателей этих знаний.

Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить? + answer_accepted: >- + Мы не рекомендуем удалять вопросы с ответами, поскольку это лишает будущих читателей этих знаний.

Повторное удаление вопросов с ответами может привести к блокировке вашей учетной записи. Вы уверены, что хотите удалить? + other: Вы уверены, что хотите удалить? + tip_answer_deleted: Этот ответ был удален + undelete_title: Восстановить сообщение + undelete_desc: Вы уверены, что хотите отменить удаление? + btns: + confirm: Подтвердить + cancel: Отменить + edit: Редактировать + save: Сохранить + delete: Удалить + undelete: Отменить удаление + list: List + unlist: Unlist + unlisted: Unlisted + login: Авторизоваться + signup: Регистрация + logout: Выйти + verify: Подтвердить + create: Create + approve: Одобрить + reject: Отклонить + skip: Пропустить + discard_draft: Удалить черновик + pinned: Закрепленный + all: Все + question: Вопрос + answer: Ответ + comment: Комментарий + refresh: Обновить + resend: Отправить повторно + deactivate: Отключить + active: Активные + suspend: Заблокировать + unsuspend: Разблокировать + close: Закрыть + reopen: Открыть повторно + ok: ОК + light: Светлая тема + dark: Темная тема + system_setting: Настройки системы + default: По умолчанию + reset: Сбросить + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Результаты поиска + keywords: Ключевые слова + options: Настройки + follow: Подписаться + following: Подписка + counts: "Результатов: {{count}}" + counts_loading: "... Results" + more: Ещё + sort_btns: + relevance: По релевантности + newest: Последние + active: Активные + score: Оценки + more: Больше + tips: + title: Советы по расширенному поиску + tag: "<1>[tag] search with a tag" + user: "<1>user:username поиск по автору" + answer: "<1>ответов:0 вопросы без ответов" + score: "<1>score:3 записи с рейтингом 3+" + question: "<1>is:question поиск по вопросам" + is_answer: "<1>ответ поиск ответов" + empty: Мы ничего не смогли найти.
Попробуйте другие или менее специфичные ключевые слова. + share: + name: Поделиться + copy: Скопировать ссылку + via: Поделитесь постом через... + copied: Скопировано + facebook: Поделиться на Facebook + twitter: Share to X + cannot_vote_for_self: Вы не можете проголосовать за свой собственный пост. + modal_confirm: + title: Ошибка... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Ваша новая учетная запись подтверждена; вы будете перенаправлены на главную страницу. + link: Перейти на главную + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Ваш адрес электронной почты был обновлен. + confirm_new_email_invalid: >- + Извините, эта ссылка для подтверждения больше недействительна. Возможно, ваш адрес электронной почты уже был изменен? + unsubscribe: + page_title: Отписаться + success_title: Вы успешно отписались от рассылки + success_desc: Вы были успешно удалены из этого списка подписчиков и больше не будете получать от нас никаких электронных писем. + link: Изменить настройки + question: + following_tags: Подписка на теги + edit: Редактировать + save: Сохранить + follow_tag_tip: Подпишитесь на теги, чтобы следить за интересующими темами. + hot_questions: Популярные вопросы + all_questions: Все вопросы + x_questions: "{{ count }} вопросов" + x_answers: "{{ count }} ответов" + x_posts: "{{ count }} Posts" + questions: Вопросы + answers: Ответы + newest: Последние + active: Активные + hot: Hot + frequent: Frequent + recommend: Recommend + score: Оценка + unanswered: Без ответа + modified: изменён + answered: отвеченные + asked: спросил(а) + closed: закрытый + follow_a_tag: Следить за тегом + more: Подробнее + personal: + overview: Обзор + answers: Ответы + answer: ответ + questions: Вопросы + question: вопрос + bookmarks: Закладки + reputation: Репутация + comments: Комментарии + votes: Голоса + badges: Badges + newest: Последние + score: Оценки + edit_profile: Редактировать профиль + visited_x_days: "Посещено {{ count }} дней" + viewed: Просмотрен + joined: Присоединился + comma: "," + last_login: Просмотрен(-а) + about_me: О себе + about_me_empty: "// Привет, Мир!" + top_answers: Лучшие ответы + top_questions: Топ вопросов + stats: Статистика + list_empty: Сообщений не найдено.
Возможно, вы хотели бы выбрать другую вкладку? + content_empty: No posts found. + accepted: Принято + answered: отвеченные + asked: спросил + downvoted: проголосовано против + mod_short: MOD + mod_long: Модераторы + x_reputation: репутация + x_votes: полученные голоса + x_answers: ответы + x_questions: вопросы + recent_badges: Recent Badges + install: + title: Installation + next: Следующий + done: Готово + config_yaml_error: Не удается создать файл config.yaml. + lang: + label: Пожалуйста, выберите язык + db_type: + label: База данных + db_username: + label: Имя пользователя + placeholder: root + msg: Имя пользователя не может быть пустым. + db_password: + label: Пароль + placeholder: root + msg: Пароль не может быть пустым. + db_host: + label: Сервер базы данных + placeholder: "db:3306" + msg: Сервер базы данных не может быть пустым. + db_name: + label: Название базы данных + placeholder: ответ + msg: Имя базы данных не может быть пустым. + db_file: + label: Файл базы данных + placeholder: /data/answer.db + msg: Файл базы данных не может быть пустым. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Создайте файл config.yaml + label: Файл config.yaml создан. + desc: >- + Вы можете создать файл <1>config.yaml вручную в каталоге <1>/var/wwww/xxx/ и вставить в него следующий текст. + info: После этого нажмите на кнопку "Далее". + site_information: Информация о сайте + admin_account: Администратор + site_name: + label: Название сайта + msg: Название сайта не может быть пустым. + msg_max_length: Длина названия сайта должна составлять не более 30 символов. + site_url: + label: Адрес сайта + text: Адрес вашего сайта. + msg: + empty: URL-адрес сайта не может быть пустым. + incorrect: Неверный формат URL-адреса сайта. + max_length: Длина URL-адреса сайта должна составлять не более 512 символов. + contact_email: + label: Контактный адрес электронной почты + text: Адрес электронной почты контактного лица, ответственного за этот сайт. + msg: + empty: Контактный адрес электронной почты не может быть пустым. + incorrect: Некорректный формат контактного адреса электронной почты. + login_required: + label: Приватный + switch: Требуется авторизация + text: Только зарегистрированные пользователи могут получить доступ к этому сообществу. + admin_name: + label: Имя + msg: Имя не может быть пустым. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Пароль + text: >- + Этот пароль понадобится вам для входа в систему. Пожалуйста, сохраните его в надежном месте. + msg: Пароль не может быть пустым. + msg_min_length: Длина пароля должна составлять не менее 8 символов. + msg_max_length: Длина пароля должна составлять не более 32 символов. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: Вам понадобится этот адрес электронной почты для входа в систему. + msg: + empty: Адрес электронной почты не может быть пустым. + incorrect: Недопустимый формат e-mail адреса. + ready_title: Your site is ready + ready_desc: >- + Если вам когда-нибудь захочется изменить дополнительные настройки, посетите <1>раздел администратора; найдите его в меню сайта. + good_luck: "Получайте удовольствие и удачи!" + warn_title: Предупреждение + warn_desc: >- + Файл <1>config.yaml уже существует. Если вам нужно сбросить любой из элементов конфигурации в этом файле, пожалуйста, удалите его. + install_now: Вы можете попробовать <1>установить сейчас. + installed: Уже установлено + installed_desc: >- + Похоже, вы уже установили. Для переустановки, пожалуйста, сначала очистите ваши старые таблицы базы данных. + db_failed: Ошибка подключения к базе данных + db_failed_desc: >- + Это означает, что информация о базе данных в вашем файле <1>config.yaml неверна, либо не удалось установить контакт с сервером базы данных. Это может означать, что сервер базы данных вашего хоста недоступен. + counts: + views: просмотры + votes: голоса + answers: ответы + accepted: Принято + page_error: + http_error: Ошибка HTTP {{ code }} + desc_403: Нет прав доступа для просмотра этой страницы. + desc_404: К сожалению, эта страница не существует. + desc_50X: Сервер обнаружил ошибку и не смог выполнить ваш запрос. + back_home: Вернуться на главную страницу + page_maintenance: + desc: "Мы выполняем техническое обслуживание, скоро вернемся." + nav_menus: + dashboard: Панель управления + contents: Содержимое + questions: Вопросы + answers: Ответы + users: Пользователи + badges: Badges + flags: Отметить + settings: Настройки + general: Основные + interface: Интерфейс + smtp: SMTP + branding: Фирменное оформление + legal: Правовая информация + write: Написать + tos: Пользовательское Соглашение + privacy: Конфиденциальность + seo: SEO + customize: Настройки интерфейса + themes: Темы + login: Вход + privileges: Привилегии + plugins: Плагины + installed_plugins: Установленные плагины + apperance: Appearance + website_welcome: Добро пожаловать на {{site_name}} + user_center: + login: Вход + qrcode_login_tip: Пожалуйста, используйте {{ agentName }} для сканирования QR-кода и входа в систему. + login_failed_email_tip: Не удалось войти в систему, пожалуйста, разрешите этому приложению получить доступ к вашей электронной почте, прежде чем повторять попытку. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Администратор + dashboard: + title: Панель управления + welcome: Welcome to Admin! + site_statistics: Статистика сайта + questions: "Вопросы:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Ответы:" + comments: "Комментарии:" + votes: "Голоса:" + users: "Пользователи:" + flags: "Жалобы:" + reviews: "Reviews:" + site_health: Здоровье сайта + version: "Версия:" + https: "HTTPS:" + upload_folder: "Каталог загрузки:" + run_mode: "Режим приватности:" + private: Приватный + public: Публичные + smtp: "SMTP:" + timezone: "Часовой пояс:" + system_info: Информация о системе + go_version: "Версия GO:" + database: "База данных:" + database_size: "Размер базы данных:" + storage_used: "Использовано хранилища: " + uptime: "Время работы:" + links: Ссылки + plugins: Плагины + github: GitHub + blog: Блог + contact: Контакты + forum: Форум + documents: Документы + feedback: Обратная связь + support: Поддержка + review: Обзор + config: Конфигурация + update_to: Обновление до + latest: Последние + check_failed: Проверка не удалась + "yes": "Да" + "no": "Нет" + not_allowed: Запрещено + allowed: Разрешено + enabled: Включено + disabled: Отключено + writable: Доступен для записи + not_writable: Не доступен для записи + flags: + title: Жалобы + pending: Ожидают + completed: Рассмотрены + flagged: Жалобы + flagged_type: Жалоба {{ type }} + created: Создано + action: Действие + review: На проверку + user_role_modal: + title: Изменить роль пользователя на... + btn_cancel: Отмена + btn_submit: Отправить + new_password_modal: + title: Задать новый пароль + form: + fields: + password: + label: Пароль + text: Сессия пользователя будет завершена и ему придется повторить вход. + msg: Длина пароля должна составлять от 8 до 32 символов. + btn_cancel: Отменить + btn_submit: Отправить + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Создание новых пользователей + form: + fields: + users: + label: Массовое добавление пользователей + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Разделите “name, email, password” запятыми. По одному пользователю в строке. + msg: "Пожалуйста, введите адрес электронной почты пользователя, по одному на строку." + display_name: + label: Отображаемое имя + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Некорректный email. + password: + label: Пароль + msg: Длина пароля должна составлять от 8 до 32 символов. + btn_cancel: Отменить + btn_submit: Отправить + users: + title: Пользователи + name: Имя + email: Email + reputation: Репутация + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Статус + role: Роль + action: Действия + change: Изменить + all: Все + staff: Сотрудники + more: Ещё + inactive: Неактивные + suspended: Заблокированные + deleted: Удаленные + normal: Обычный + Moderator: Модератор + Admin: Администратор + User: Пользователь + filter: + placeholder: "Фильтровать по имени, user:id" + set_new_password: Задать новый пароль + edit_profile: Edit profile + change_status: Изменить статус + change_role: Изменить роль + show_logs: Показать логи + add_user: Добавить пользователя + deactivate_user: + title: Деактивировать пользователя + content: Неактивный пользователь должен будет повторно подтвердить свою электронную почту. + delete_user: + title: Удалить этого пользователя + content: Вы уверены, что хотите удалить этого пользователя? Это действие необратимо! + remove: Удалить контент пользователя (опционально) + label: Удалить все вопросы, ответы, комментарии и т.д. + text: Не устанавливайте этот флажок, если вы хотите удалить только учетную запись пользователя. + suspend_user: + title: Заблокировать этого пользователя + content: Заблокированный пользователь не сможет войти. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Вопросы + unlisted: Unlisted + post: Публикация + votes: Голоса + answers: Ответы + created: Создан + status: Статус + action: Действие + change: Изменить + pending: Ожидают + filter: + placeholder: "Фильтровать по заголовку, question:id" + answers: + page_title: Ответы + post: Публикация + votes: Голоса + created: Создан + status: Статус + action: Действие + change: Изменить + filter: + placeholder: "Фильтровать по заголовку, answer:id" + general: + page_title: Основные + name: + label: Название сайта + msg: Название сайта не может быть пустым. + text: "Название сайта, используемое в теге title." + site_url: + label: URL-адрес сайта + msg: URL-адрес сайта не может быть пустым. + validate: Пожалуйста, введите корректный URL. + text: Адрес вашего сайта. + short_desc: + label: Краткое описание + msg: Краткое описание сайта не может быть пустым. + text: "Краткое описание, используемое в теге заголовка на домашней странице." + desc: + label: Описание сайта + msg: Описание сайта не может быть пустым. + text: "Опишите этот сайт одним предложением, как используется в теге meta description" + contact_email: + label: Контактный адрес электронной почты + msg: Контактный адрес электронной почты не может быть пустым. + validate: Контактный адрес электронной почты не может быть пустым. + text: Адрес электронной почты контактного лица, ответственного за данный сайт. + check_update: + label: Обновления программного обеспечения + text: Автоматически проверять наличие обновлений + interface: + page_title: Интерфейс + language: + label: Язык интерфейса + msg: Язык интерфейса не может быть пустым. + text: Язык пользовательского интерфейса. Он изменится при обновлении страницы. + time_zone: + label: Часовой пояс + msg: Часовой пояс не может быть пустым. + text: Выберите город в том же часовом поясе, что и вы. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: С эл. почты + msg: Адрес электронной почты отправителя не может быть пустым. + text: Адрес электронной почты, с которого отправляются письма. + from_name: + label: Имя отправителя + msg: Имя пользователя не может быть пустым. + text: Имя, с которого отправляются электронные письма. + smtp_host: + label: Сервер SMTP + msg: Сервер SMTP не может быть пустым. + text: Ваш почтовый сервер. + encryption: + label: Шифрование + msg: Шифрование не может быть пустым. + text: Для большинства серверов рекомендуется использовать протокол SSL. + ssl: SSL + tls: TLS + none: Нет + smtp_port: + label: Порт SMTP + msg: Порт SMTP должен быть числом 1 ~ 65535. + text: Порт для вашего почтового сервера. + smtp_username: + label: Имя пользователя SMTP + msg: Имя пользователя SMTP не может быть пустым. + smtp_password: + label: Пароль SMTP + msg: Пароль SMTP не может быть пустым. + test_email_recipient: + label: Тестовые получатели электронной почты + text: Укажите адрес электронной почты, на который будут отправляться тестовые сообщения. + msg: Некорректный тестовый адрес электронной почты + smtp_authentication: + label: Включить авторизацию + title: Аутентификация SMTP + msg: Аутентификационные данные для SMTP не могут быть пустыми. + "yes": "Да" + "no": "Нет" + branding: + page_title: Фирменное оформление + logo: + label: Логотип + msg: Логотип не может быть пустым. + text: Изображение логотипа в левом верхнем углу вашего сайта. Используйте широкое прямоугольное изображение высотой 56 см с соотношением сторон более 3:1. Если оставить поле пустым, будет показан текст заголовка сайта. + mobile_logo: + label: Мобильный логотип + text: Логотип, используемый в мобильной версии вашего сайта. Используйте широкое прямоугольное изображение высотой 56. Если оставить пустым, будет использоваться изображение из настройки "Логотип". + square_icon: + label: Квадратный значок + msg: Square icon не может быть пустым. + text: Изображение, используемое в качестве основы для значков метаданных. В идеале должно быть больше 512x512. + favicon: + label: Иконка + text: Значок для вашего сайта. Для корректной работы через CDN он должен быть в формате png. Размер будет изменен до 32x32. Если оставить пустым, будет использоваться "square icon". + legal: + page_title: Правовая информация + terms_of_service: + label: Условия использования + text: "Вы можете добавить содержимое условий предоставления услуг здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь." + privacy_policy: + label: Условия конфиденциальности + text: "Вы можете добавить содержание политики конфиденциальности здесь. Если у вас уже есть документ, размещенный в другом месте, укажите полный URL-адрес здесь." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Написать + restrict_answer: + title: Answer write + label: Каждый пользователь может написать только один ответ на каждый вопрос + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Рекомендованные теги + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Каждый новый вопрос должен иметь хотя бы один рекомендуемый тег." + reserved_tags: + label: Зарезервированные теги + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Постоянная ссылка + text: Пользовательские структуры URL-адресов могут улучшить удобство использования и обратную совместимость ваших ссылок. + robots: + label: robots.txt + text: Это приведет к необратимому переопределению всех связанных настроек сайта. + themes: + page_title: Темы + themes: + label: Темы + text: Выберите существующую тему. + color_scheme: + label: Цветовая схема + navbar_style: + label: Navbar background style + primary_color: + label: Основной цвет + text: Измените цвета, используемые в ваших темах + css_and_html: + page_title: CSS и HTML + custom_css: + label: Пользовательский CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Нижняя панель + text: Это будет вставлено перед </body>. + sidebar: + label: Боковая панель + text: Это будет вставлено в боковую панель. + login: + page_title: Авторизоваться + membership: + title: Участие в сообществах + label: Разрешить новые регистрации + text: Отключите, чтобы никто не мог создать новую учетную запись. + email_registration: + title: Регистрация по электронной почте + label: Разрешить регистрацию по электронной почте + text: Отключите, чтобы предотвратить создание новой учетной записи через электронную почту. + allowed_email_domains: + title: Разрешенные домены электронной почты + text: Домены электронной почты, с которыми пользователи должны регистрировать аккаунты. Один домен на каждой строке. Игнорируется, если пусто. + private: + title: Приватный + label: Требуется авторизация + text: Только зарегистрированные пользователи могут получить доступ к этому сообществу. + password_login: + title: Вход в пароль + label: Разрешить вход по паролю + text: "Предупреждение: При отключении, вы не сможете войти, если ранее не настроили другой способ входа." + installed_plugins: + title: Установленные плагины + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: Все + active: Активные + inactive: Неактивные + outdated: Устаревшие + plugins: + label: Плагины + text: Выберите существующий плагин. + name: Название + version: Версия + status: Статус + action: Действие + deactivate: Деактивировать + activate: Активировать + settings: Настройки + settings_users: + title: Пользователи + avatar: + label: Аватар по умолчанию + text: Для пользователей, у которых нет собственного пользовательского аватара. + gravatar_base_url: + label: Базовый URL Gravatar + text: URL базы API провайдера Gravatar. Игнорируется, если пусто. + profile_editable: + title: Настройки профилей + allow_update_display_name: + label: Разрешить пользователям изменять отображаемое имя + allow_update_username: + label: Разрешить пользователям изменять свой username + allow_update_avatar: + label: Разрешить пользователям изменять изображение своего профиля + allow_update_bio: + label: Разрешить пользователям изменять свои сведения в поле "обо мне" + allow_update_website: + label: Разрешить пользователям изменять свой веб-сайт + allow_update_location: + label: Разрешить пользователям изменять свое местоположение + privilege: + title: Привилегии + level: + label: Необходимый уровень репутации + text: Выберите количество репутации, необходимое для получения привилегий + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (опционально) + empty: не может быть пустым + invalid: недействителен + btn_submit: Сохранить + not_found_props: "Требуемое свойство {{ key }} не найдено." + select: Select + page_review: + review: На проверку + proposed: предложенный + question_edit: Редактировать вопрос + answer_edit: Редактирование ответа + tag_edit: Редактирование тега + edit_summary: Редактирование краткого описания + edit_question: Редактирование вопроса + edit_answer: Редактирование ответа + edit_tag: Редактирование тега + empty: Нет задач для проверки. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Предложенные исправления + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: репутация + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: Восстановлен + deleted: Удаленные + downvote: бесполезный + upvote: оценить + accept: принять + cancelled: отменен + commented: прокомментированный + rollback: откатить + edited: отредактированный + answered: отвеченные + asked: asked + closed: закрытый + reopened: Открыт повторно + created: созданный + pin: закрепленный + unpin: незакреплённые + show: listed + hide: unlisted + title: "History for" + tag_title: "Хронология" + show_votes: "Show votes" + n_or_a: Недоступно + title_for_question: "Хронология" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Дата и время + type: Тип + by: Автор + comment: Комментарий + no_data: "Ничего не найдено." + users: + title: Пользователи + users_with_the_most_reputation: Пользователи с самой высокой репутацией на этой неделе + users_with_the_most_vote: Пользователи, которые больше всего проголосовали на этой неделе + staffs: Сотрудники нашего сообщества + reputation: репутация + votes: голоса + prompt: + leave_page: Вы уверены, что хотите покинуть страницу? + changes_not_save: Ваши изменения могут не быть сохранены. + draft: + discard_confirm: Вы уверены, что хотите отказаться от своего черновика? + messages: + post_deleted: Этот пост был удалён. + post_cancel_deleted: This post has been undeleted. + post_pin: Этот пост был закреплен. + post_unpin: Этот пост был откреплен. + post_hide_list: Это сообщение было скрыто из списка. + post_show_list: Этот пост был показан в списке. + post_reopen: Этот пост был вновь открыт. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/sk_SK.yaml b/i18n/sk_SK.yaml new file mode 100644 index 000000000..40ffcb163 --- /dev/null +++ b/i18n/sk_SK.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Úspech. + unknown: + other: Neznáma chyba. + request_format_error: + other: Formát žiadosti nie je platný. + unauthorized_error: + other: Neoprávnené. + database_error: + other: Chyba dátového servera. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: List + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: Užívateľ + admin: + other: Správca + moderator: + other: Moderátor + description: + user: + other: Predvolené bez špeciálneho prístupu. + admin: + other: Má plnú moc a prístup ku stránke. + moderator: + other: Má prístup ku všetkým príspevkom okrem nastavenia správcu. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: E-mail + e_mail: + other: Email + password: + other: Heslo + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: E-mail a heslo sa nezhodujú. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: Svoje heslo upraviť. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: Nemôžete upraviť svoj stav. + email_or_password_wrong: + other: E-mail a heslo sa nezhodujú. + answer: + not_found: + other: Odpoveď sa nenašla. + cannot_deleted: + other: Žiadne povolenie na odstránenie. + cannot_update: + other: Žiadne povolenie na aktualizáciu. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Komentár nie je dovolené upravovať. + not_found: + other: Komentár sa nenašiel. + cannot_edit_after_deadline: + other: Čas na úpravu komentára bol príliš dlhý. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: E-mail už existuje. + need_to_be_verified: + other: E-mail by sa mal overiť. + verify_url_expired: + other: Platnosť overenej adresy URL e-mailu vypršala, pošlite e-mail znova. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Jazykový súbor sa nenašiel. + object: + captcha_verification_failed: + other: Captcha zle. + disallow_follow: + other: Nemáte dovolené sledovať. + disallow_vote: + other: Nemáte povolené hlasovať. + disallow_vote_your_self: + other: Nemôžete hlasovať za svoj vlastný príspevok. + not_found: + other: Objekt sa nenašiel. + verification_failed: + other: Overenie zlyhalo. + email_or_password_incorrect: + other: E-mail a heslo sa nezhodujú. + old_password_verification_failed: + other: Overenie starého hesla zlyhalo + new_password_same_as_previous_setting: + other: Nové heslo je rovnaké ako predchádzajúce. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: Tento príspevok bol odstránený. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Otázka sa nenašla. + cannot_deleted: + other: Žiadne povolenie na odstránenie. + cannot_close: + other: Žiadne povolenie na uzavretie. + cannot_update: + other: Žiadne povolenie na aktualizáciu. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Spracovanie prehľadu zlyhalo. + not_found: + other: Hlásenie sa nenašlo. + tag: + already_exist: + other: Značka už existuje. + not_found: + other: Značka sa nenašla. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Zadajte aspoň jednu požadovanú značku. + not_contain_synonym_tags: + other: Nemal by obsahovať synonymické značky. + cannot_update: + other: Žiadne povolenie na aktualizáciu. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: Synonymum aktuálnej značky nemôžete nastaviť ako samotnú. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Téma sa nenašla. + revision: + review_underway: + other: Momentálne nie je možné upravovať, vo fronte na kontrolu je verzia. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: E-mail a heslo sa nezhodujú. + not_found: + other: Používateľ nenájdený. + suspended: + other: Používateľ bol pozastavený. + username_invalid: + other: Používateľské meno je neplatné. + username_duplicate: + other: Používateľské meno sa už používa. + set_avatar: + other: Nastavenie avatara zlyhalo. + cannot_update_your_role: + other: Svoju rolu nemôžete zmeniť. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read Config zlyhal + database: + connection_failed: + other: Databázové pripojenie zlyhalo + create_table_failed: + other: Vytvorenie tabuľky zlyhalo + install: + create_config_failed: + other: Nie je možné vytvoriť súbor config.yaml. + upload: + unsupported_file_format: + other: Nepodporovaný formát súboru. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: nevyžiadaná pošta + desc: + other: Táto otázka už bola položená a už má odpoveď. + guideline: + name: + other: dôvod špecifický pre komunitu + desc: + other: Táto otázka nespĺňa pokyny pre komunitu. + multiple: + name: + other: potrebuje podrobnosti alebo jasnosť + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: niečo iné + desc: + other: Tento príspevok vyžaduje iný dôvod, ktorý nie je uvedený vyššie. + operation_type: + asked: + other: požiadaný + answered: + other: zodpovedaný + modified: + other: upravený + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: aktualizovaná otázka + answer_the_question: + other: zodpovedaná otázka + update_answer: + other: aktualizovaná odpoveď + accept_answer: + other: prijatá odpoveď + comment_question: + other: komentovaná otázka + comment_answer: + other: komentovaná odpoveď + reply_to_you: + other: odpovedal vám + mention_you: + other: spomenul vás + your_question_is_closed: + other: Vaša otázka bola uzavretá + your_question_was_deleted: + other: Vaša otázka bola odstránená + your_answer_was_deleted: + other: Vaša odpoveď bola odstránená + your_comment_was_deleted: + other: Váš komentár bol odstránený + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n

{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Ako formátovať + desc: >- + + pagination: + prev: Predch + next: Ďalšie + page_title: + question: Otázka + questions: Otázky + tag: Značka + tags: Značky + tag_wiki: značka wiki + create_tag: Vytvoriť štítok + edit_tag: Upraviť značku + ask_a_question: Create Question + edit_question: Úpraviť otázku + edit_answer: Úpraviť odpoveť + search: Vyhľadávanie + posts_containing: Príspevky obsahujúce + settings: Nastavenie + notifications: Oznámenia + login: Prihlásiť sa + sign_up: Prihlásiť Se + account_recovery: Obnovenie účtu + account_activation: Aktivácia účtu + confirm_email: Potvrď e-mail + account_suspended: Účet pozastavený + admin: Administrátor + change_email: Upraviť e-mail + install: Odpoveď Inštalácia + upgrade: Answer Upgrade + maintenance: Údržba webových stránok + users: Užívatelia + oauth_callback: Processing + http_404: HTTP chyba 404 + http_50X: HTTP chyba 403 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: Oznámenia + inbox: Doručená pošta + achievement: Úspechy + new_alerts: New alerts + all_read: Označiť všetko ako prečítané + show_more: Zobraziť viac + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Váš účet bol pozastavený + until_time: "Váš účet bol pozastavený do {{ time }}." + forever: Tento používateľ bol navždy pozastavený. + end: Nespĺňate pokyny pre komunitu. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Silný + chart: + text: Rebríček + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Ganttov diagram + pie_chart: Koláčový graf + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Kód + msg: + empty: Code cannot be empty. + language: + label: Jazyk + placeholder: Automatic detection + btn_cancel: Zrušiť + btn_confirm: Pridať + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Pomoc + hr: + text: Horizontal rule + image: + text: Obrázok + add_image: Pridať obrázok + tab_image: Nahrať obrázok + form_image: + fields: + file: + label: Image file + btn: Vyberte obrázok + msg: + empty: Názov súboru nemôže byť prázdny. + only_image: Povolené sú iba obrázkové súbory. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Popis + tab_url: URL obrázka + form_url: + fields: + url: + label: URL obrázka + msg: + empty: URL obrázka nemôže byť prázdna. + name: + label: Description + btn_cancel: Zrušiť + btn_confirm: Pridať + uploading: Nahráva sa + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hypertextový odkaz + add_link: Pridať hypertextový odkaz + form: + fields: + url: + label: URL + msg: + empty: URL adresa nemôže byť prázdna. + name: + label: Popis + btn_cancel: Zrušiť + btn_confirm: Pridať + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Bunka + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: Tento príspevok uzatváram ako... + btn_cancel: Zrušiť + btn_submit: Potvrdiť + remark: + empty: Nemôže byť prázdny. + msg: + empty: Vyberte dôvod. + report_modal: + flag_title: Nahlasujem nahlásenie tohto príspevku ako... + close_title: Tento príspevok zatváram ako ... + review_question_title: Kontrola otázky + review_answer_title: Kontrola odpovede + review_comment_title: Kontrola komentára + btn_cancel: Zrušiť + btn_submit: Potvrdiť + remark: + empty: Nemôže byť prázdny. + msg: + empty: Vyberte dôvod. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Vytvorte novú značku + form: + fields: + display_name: + label: Display name + msg: + empty: Zobrazovaný názov nemôže byť prázdny. + range: Zobrazovaný názov do 35 znakov. + slug_name: + label: URL slug + desc: URL slug do 35 znakov. + msg: + empty: URL slug nemôže byť prázdny. + range: URL slug do 35 znakov. + character: URL slug obsahuje nepovolenú znakovú sadu. + desc: + label: Opis + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Zrušiť + btn_submit: Potvrdiť + btn_post: Post new tag + tag_info: + created_at: Vytvorená + edited_at: Upravená + history: História + synonyms: + title: Synonymá + text: Nasledujúce značky budú premapované na + empty: Nenašli sa žiadne synonymá. + btn_add: Pridajte synonymum + btn_edit: Upraviť + btn_save: Uložiť + synonyms_text: Nasledujúce značky budú premapované na + delete: + title: Odstrániť túto značku + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Naozaj chcete odstrániť? + close: Zavrieť + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Upraviť značku + default_reason: Upraviť značku + default_first_reason: Add tag + btn_save_edits: Uložiť úpravy + btn_cancel: Zrušiť + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [o] HH:mm" + now: teraz + x_seconds_ago: "pred {{count}}s" + x_minutes_ago: "pred {{count}}m" + x_hours_ago: "pred {{count}}h" + hour: hodina + day: deň + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Pridať komentár + reply_to: Odpovedať + btn_reply: Odpovedať + btn_edit: Upraviť + btn_delete: Zmazať + btn_flag: Vlajka + btn_save_edits: Uložiť zmeny + btn_cancel: Zrušiť + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Uprav odpoveď + default_reason: Uprav odpoveď + default_first_reason: Add answer + form: + fields: + revision: + label: Revízia + answer: + label: Odpoveď + feedback: + characters: Obsah musí mať dĺžku najmenej 6 znakov. + edit_summary: + label: Edit summary + placeholder: >- + Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie) + btn_save_edits: Uložiť úpravy + btn_cancel: Zrušiť + tags: + title: Značky + sort_buttons: + popular: Populárne + name: názov + newest: Newest + button_follow: Sledovať + button_following: Sledované + tag_label: otázky + search_placeholder: Filtrujte podľa názvu značky + no_desc: Značka nemá popis. + more: Viac + wiki: Wiki + ask: + title: Create Question + edit_title: Upraviť otázku + default_reason: Upraviť otázku + default_first_reason: Create question + similar_questions: Podobné otázky + form: + fields: + revision: + label: Revízia + title: + label: Názov + placeholder: What's your topic? Be specific. + msg: + empty: Názov nemôže byť prázdny. + range: Názov do 150 znakov + body: + label: Telo + msg: + empty: Telo nemôže byť prázdne. + tags: + label: Značky -- + msg: + empty: Štítky nemôžu byť prázdne. + answer: + label: Odpoveď + msg: + empty: Odpoveď nemôže byť prázdna. + edit_summary: + label: Edit summary + placeholder: >- + Stručne vysvetlite svoje zmeny (opravený pravopis, opravená gramatika, vylepšené formátovanie) + btn_post_question: Uverejnite svoju otázku + btn_save_edits: Uložiť úpravy + answer_question: Odpovedzte na svoju vlastnú otázku + post_question&answer: Uverejnite svoju otázku a odpoveď + tag_selector: + add_btn: Pridať značku + create_btn: Vytvoriť novú značku + search_tag: Vyhľadať značku -- + hint: "Describe what your content is about, at least one tag is required." + no_result: Nezodpovedajú žiadne značky + tag_required_text: Povinný štítok (aspoň jeden) + header: + nav: + question: Otázky + tag: Značky + user: Užívatelia + badges: Badges + profile: Profil + setting: Nastavenia + logout: Odhlásiť sa + admin: Správca + review: Preskúmanie + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Vyhľadávanie + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Zmena + loading: načítavanie... + pic_auth_code: + title: captcha + placeholder: Zadajte vyššie uvedený text + msg: + empty: Captcha nemôže byť prázdna. + inactive: + first: >- + Ste takmer na konci! Poslali sme Vám aktivačný mail na adresu {{mail}}. K aktivácií účtu postupujte prosím podľa pokynov v e-maily. + info: "Ak neprichádza, skontrolujte priečinok spamu." + another: >- + Poslali sme vám ďalší aktivačný e-mail na adresu {{mail}}. Môže to trvať niekoľko minút; Nezabudnite skontrolovať priečinok spamu. + btn_name: Opätovne odoslať aktivačný e-mail + change_btn_name: Zmeniť e-mail + msg: + empty: Nemôže byť prázdny. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Pre pokračovanie sa prihláste + info_sign: Nemáte účet? <1>Sign up + info_login: Máte už účet? <1>Log in + agreements: Registráciou súhlasíte s <1>zásadami ochrany osobných údajov a <3>podmienkami služby. + forgot_pass: Zabudli ste heslo? + name: + label: Prihlasovacie meno + msg: + empty: Prihlasovacie meno nemôže byť prázdne. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: E-mail + msg: + empty: E-mail nemôže byť prázdny. + password: + label: Heslo + msg: + empty: Heslo nemôže byť prázdne. + different: Heslá zadané na oboch stranách sú nekonzistentné + account_forgot: + page_title: Zabudli ste heslo + btn_name: Pošlite mi e-mail na obnovenie + send_success: >- + Ak sa účet zhoduje s {{mail}}, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo. + email: + label: E-mail + msg: + empty: E-mail nemôže byť prázdny. + change_email: + btn_cancel: Zrušiť + btn_update: Aktualizovať e-mailovú adresu + send_success: >- + Ak sa účet zhoduje s {{mail}}, tak by ste mali čoskoro dostať e-mail s pokynmi, ako resetovať svoje heslo. + email: + label: New email + msg: + empty: E-mail nemôže byť prázdny. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Resetovanie hesla + btn_name: Obnoviť heslo + reset_success: >- + Úspešne ste zmenili svoje heslo; Budete presmerovaný na prihlásenie. + link_invalid: >- + Ospravedlňujeme sa, tento odkaz na obnovenie hesla už nie je platný. Možno už došlo k resetovaniu vašho hesla? + to_login: Continue to log in page + password: + label: Heslo + msg: + empty: Heslo nemôže byť prázdne. + length: Dĺžka musí byť medzi 8 a 32 + different: Heslá zadané na oboch stranách sú nekonzistentné + password_confirm: + label: Confirm new password + settings: + page_title: Nastavenia + goto_modify: Go to modify + nav: + profile: Profil + notification: Oznámenia + account: Účet + interface: Rozhranie + profile: + heading: Profil + btn_name: Uložiť + display_name: + label: Display name + msg: Zobrazované meno nemôže byť prázdne. + msg_range: Display name must be 2-30 characters in length. + username: + label: Užívateľské meno + caption: Ľudia vás môžu spomenúť ako „@používateľské meno“. + msg: Užívateľské meno nemôže byť prázdne. + msg_range: Username must be 2-30 characters in length. + character: 'Musíte použiť znakovú sadu "a-z", "0-9", "- . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Vlastný + custom_text: Môžete nahrať svoj obrázok. + default: Systém + msg: Nahrajte avatara prosím + bio: + label: About me + website: + label: Webová stránka + placeholder: "https://priklad.com" + msg: Nesprávny formát webovej stránky + location: + label: Poloha + placeholder: "Mesto, Krajina" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Účet + change_email_btn: Zmeniť e-mail + change_pass_btn: Zmeniť heslo + change_email_info: >- + Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Heslo + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: Dĺžka musí byť medzi 8 a 32. + different: Dve zadané heslá sa nezhodujú. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Rozhranie + lang: + label: Interface language + text: Jazyk používateľského rozhrania. Zmení sa pri obnove stránky. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: aktualizácia úspešna + update_password: Heslo bolo úspešne zmenené. + flag_success: Ďakujeme za nahlásenie. + forbidden_operate_self: Zakázané operovať seba + review: Vaša revízia sa zobrazí po preskúmaní. + sent_success: Sent successfully + related_question: + title: Related + answers: odpovede + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Opýtané + asked: opýtané + update: Aktualizované + edit: upravené + commented: commented + Views: Videné + Follow: Sledovať + Following: Sledované + follow_tip: Follow this question to receive notifications + answered: zodpovedaný + closed_in: Uzatvorené + show_exist: Ukázať existujúcu otázku. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Odpovede + score: Skóre + newest: Najnovšie + oldest: Oldest + btn_accept: Súhlasiť + btn_accepted: Prijaté + write_answer: + title: Vaša odpoveď + edit_answer: Edit my existing answer + btn_name: Pošlite svoju odpoveď + add_another_answer: Pridajte ďalšiu odpoveď + confirm_title: Pokračovať v odpovedi + continue: Pokračovať + confirm_info: >- +

Ste si istí, že chcete pridať ďalšiu odpoveď?

Mohli by ste namiesto toho použiť úpravu na vylepšenie svojej už existujúcej odpovede.

+ empty: Odpoveď nemôže byť prázdna. + characters: Minimálna dĺžka obsahu musí byť 6 znakov. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Znovu otvoriť tento príspevok + content: Ste si istý, že ho chcete znovu otvoriť? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Odstrániť tento príspevok + question: >- + Neodporúčame mazanie otázok s odpoveďmi pretože týmto oberáte budúcich čitateľov o tieto vedomostí.

Opakované mazanie zodpovedaných otázok môže mať za následok zablokovanie možnosti kladenia otázok z vášho účtu. Ste si istí, že chcete otázku odstrániť? + answer_accepted: >- +

Neodporúčame odstránenie akceptovanej odpovede pretože týmto oberáte budúcich čitateľov o tieto vedomostí.

Opakované mazanie akceptovaných odpovedí môže mať za následok zablokovanie možnosti odpovedať z vášho účtu. Ste si istí, že chcete odstrániť odpoveď? + other: Ste si istí, že ju chcete odstrániť? + tip_answer_deleted: Táto odpoveď bola odstránená + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Potvrdiť + cancel: Zrušiť + edit: Edit + save: Uložiť + delete: Vymazať + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Prihlásiť sa + signup: Registrovať sa + logout: Odhlásiť sa + verify: Preveriť + create: Create + approve: Schváliť + reject: Odmietnuť + skip: Preskočiť + discard_draft: Zahodiť koncept + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Výsledky vyhľadávania + keywords: Kľúčové slová + options: možnosti + follow: Sledovať + following: Sledované + counts: "{{count}} výsledky" + counts_loading: "... Results" + more: Viac + sort_btns: + relevance: Relevantnosť + newest: Najnovšie + active: Aktívne + score: Skóre + more: Viac + tips: + title: Tipy na pokročilé vyhľadávanie + tag: "<1>[tag] hľadať v rámci značky" + user: "<1>user:username hľadať podľa autora" + answer: "<1>answers:0 nezodpovedané otázky" + score: "<1>score:3 Príspevky so skóre 3+" + question: "<1>is:question hľadať otázky" + is_answer: "<1>is:answer hľadať odpovede" + empty: Nemohli sme nič nájsť.
Vyskúšajte iné alebo menej špecifické kľúčové slová. + share: + name: Zdieľať + copy: Skopírovať odkaz + via: Zdieľajte príspevok cez... + copied: Skopírované + facebook: Zdieľať na Facebooku + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Chyba... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Váš nový účet je potvrdený; Budete presmerovaný na domovskú stránku. + link: Pokračovať na domovskú stránku + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Váš e-mail bol aktualizovaný. + confirm_new_email_invalid: >- + Ospravedlňujeme sa, tento potvrdzovací odkaz už nie je platný. Váš e-mail je už môžno zmenený. + unsubscribe: + page_title: Zrušiť odber + success_title: Úspešne zrušenie odberu + success_desc: Boli ste úspešne odstránený zo zoznamu odoberateľov a nebudete od nás dostávať žiadne ďalšie e-maily. + link: Zmeniť nastavenia + question: + following_tags: Nasledujúce značky + edit: Upraviť + save: Uložiť + follow_tag_tip: Postupujte podľa značiek a upravte si zoznam otázok. + hot_questions: Najlepšie otázky + all_questions: Všetky otázky + x_questions: "{{ count }} otázky/otázok" + x_answers: "{{ count }} odpovede/odpovedí" + x_posts: "{{ count }} Posts" + questions: Otázky + answers: Odpovede + newest: Najnovšie + active: Aktívne + hot: Hot + frequent: Frequent + recommend: Recommend + score: Skóre + unanswered: Nezodpovedané + modified: upravené + answered: zodpovedané + asked: opýtané + closed: uzatvorené + follow_a_tag: Postupujte podľa značky + more: Viac + personal: + overview: Prehľad + answers: Odpovede + answer: odpoveď + questions: Otázky + question: otázka + bookmarks: Záložky + reputation: Reputácia + comments: Komentáre + votes: Hlasovanie + badges: Badges + newest: Najnovšie + score: Skóre + edit_profile: Edit profile + visited_x_days: "Navštívené {{ count }} dni" + viewed: Videné + joined: Pripojené + comma: "," + last_login: Videné + about_me: O mne + about_me_empty: "// Dobrý deň, svet!" + top_answers: Najlepšie odpovede + top_questions: Najlepšie otázky + stats: Štatistiky + list_empty: Nenašli sa žiadne príspevky.
Možno by ste chceli vybrať inú kartu? + content_empty: No posts found. + accepted: Prijaté + answered: zodpovedané + asked: opýtané + downvoted: downvoted + mod_short: MOD + mod_long: Moderátori + x_reputation: reputácia + x_votes: prijatých hlasov + x_answers: odpovede + x_questions: otázky + recent_badges: Recent Badges + install: + title: Installation + next: Ďalšie + done: Hotový + config_yaml_error: Nie je možné vytvoriť súbor config.yaml. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Užívateľské meno + placeholder: super užívateľ + msg: Užívateľské meno nemôže byť prázdne. + db_password: + label: Heslo + placeholder: super užívateľ + msg: Heslo nemôže byť prázdne. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: odpoveď + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Vytvoriť config.yaml + label: Vytvorený súbor Config.yaml. + desc: >- + Súbor <1>config.yaml môžete vytvoriť manuálne v adresári <1>/var/www/xxx/ a vložiť doň nasledujúci text. + info: Potom, čo ste to urobili, kliknite na tlačidlo „Ďalej“. + site_information: Informácie o stránke + admin_account: Správca + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: URL stránky + text: Adresa vašej stránky. + msg: + empty: URL stránky nemôže byť prázdny. + incorrect: Nesprávny formát adresy URL. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: E-mailová adresa kontaktu zodpovedného za túto stránku. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Meno + msg: Meno nemôže byť prázdne. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Heslo + text: >- + Na prihlásenie budete potrebovať toto heslo. Uložte si ho na bezpečné miesto. + msg: Heslo nemôže byť prázdne. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: E-mail + text: Na prihlásenie budete potrebovať tento e-mail. + msg: + empty: E-mail nemôže byť prázdny. + incorrect: Nesprávny formát e-mailu + ready_title: Your site is ready + ready_desc: >- + Ak niekedy budete chcieť zmeniť viac nastavení, navštívte stránku <1>admin section; Nájdete ju v ponuke stránok. + good_luck: "„Bavte sa a veľa šťastia!“" + warn_title: Upozornenie + warn_desc: >- + Súbor <1>config.yaml už existuje. Ak potrebujete resetovať niektorú z konfiguračných položiek v tomto súbore, najskôr ju odstráňte. + install_now: Môžete skúsiť <1>installing now. + installed: Už nainštalované + installed_desc: >- + Zdá sa, že ste už aplikáciu answer nainštalovali. Ak chcete aplikáciu preinštalovať, najprv vymažte staré tabuľky z databázy. + db_failed: Databázové pripojenie zlyhalo + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: názory + votes: hlasy + answers: odpovede + accepted: prijaté + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "Prebieha údržba, čoskoro sa vrátime." + nav_menus: + dashboard: Nástenka + contents: Obsah + questions: Otázky + answers: Odpovede + users: Užívatelia + badges: Badges + flags: Vlajky + settings: Nastavenia + general: Všeobecné + interface: Rozhranie + smtp: SMTP + branding: Budovanie značky + legal: legálne + write: písať + tos: Podmienky služby + privacy: Súkromie + seo: SEO + customize: Prispôsobiť + themes: Témy + login: Prihlásiť sa + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Administrátor + dashboard: + title: Nástenka + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Otázky:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Odpovede:" + comments: "Komentáre:" + votes: "Hlasy:" + users: "Users:" + flags: "Vlajky:" + reviews: "Reviews:" + site_health: Site health + version: "Verzia:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Časové pásmo:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Použité úložisko:" + uptime: "Doba prevádzky:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Dokumenty + feedback: Spätná väzba + support: Podpora + review: Preskúmanie + config: Konfigurácia + update_to: Aktualizovať na + latest: Posledné + check_failed: Skontrolovať zlyhanie + "yes": "Áno" + "no": "Nie" + not_allowed: Nepovolené + allowed: Povolené + enabled: Povolené + disabled: Zablokované + writable: Writable + not_writable: Not writable + flags: + title: Vlajky + pending: Prebiehajúce + completed: Dokončené + flagged: Označené + flagged_type: Flagged {{ type }} + created: Vytvorené + action: Akcia + review: Preskúmanie + user_role_modal: + title: Zmeňte rolu používateľa na... + btn_cancel: Zrušiť + btn_submit: Odovzdať + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Používatelia + name: Meno + email: E-mail + reputation: Reputácia + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Stav + role: Rola + action: Akcia + change: Zmena + all: Všetko + staff: Personál + more: More + inactive: Neaktívne + suspended: Pozastavené + deleted: Vymazané + normal: Normálné + Moderator: Moderátor + Admin: Správca + User: Používateľ + filter: + placeholder: "Filter podľa mena, používateľ: ID" + set_new_password: Nastaviť nové heslo + edit_profile: Edit profile + change_status: Zmentiť stavu + change_role: Zmeniť rolu + show_logs: Zobraziť protokoly + add_user: Pridať používateľa + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Otázky + unlisted: Unlisted + post: poslané + votes: Hlasy + answers: Odpovede + created: Vytvorené + status: Stav + action: Akcia + change: Zmena + pending: Pending + filter: + placeholder: "Filter podľa názvu, otázka:id" + answers: + page_title: Odpovede + post: Poslané + votes: Hlasy + created: Vytvorené + status: Stav + action: Akcia + change: Zmena + filter: + placeholder: "Filter podľa názvu, odpoveď:id" + general: + page_title: Všeobecné + name: + label: Site name + msg: Názov stránky nemôže byť prázdny. + text: "Názov tejto lokality, ako sa používa v značke názvu." + site_url: + label: URL stránky + msg: Adresa Url stránky nemôže byť prázdna. + validate: Prosím uveďte platnú webovú adresu. + text: Adresa vašej stránky. + short_desc: + label: Short site description + msg: Krátky popis stránky nemôže byť prázdny. + text: "Krátky popis, ako sa používa v značke názvu na domovskej stránke." + desc: + label: Site description + msg: Popis stránky nemôže byť prázdny. + text: "Opíšte túto stránku jednou vetou, ako sa používa v značke meta description." + contact_email: + label: Contact email + msg: Kontaktný e-mail nemôže byť prázdny. + validate: Kontaktný e-mail je neplatný. + text: E-mailová adresa kontaktu zodpovedného za túto stránku. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Rozhranie + language: + label: Interface language + msg: Jazyk rozhrania nemôže byť prázdny. + text: Jazyk používateľského rozhrania. Zmení sa, keď stránku obnovíte. + time_zone: + label: Časové pásmo + msg: Časové pásmo nemôže byť prázdne. + text: Vyberte si mesto v rovnakom časovom pásme ako vy. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: Z e-mailu nemôže byť prázdne. + text: E-mailová adresa, z ktorej sa odosielajú e-maily. + from_name: + label: From name + msg: Názov od nemôže byť prázdny. + text: Meno, z ktorého sa odosielajú e-maily. + smtp_host: + label: SMTP host + msg: Hostiteľ SMTP nemôže byť prázdny. + text: Váš mailový server. + encryption: + label: Šifrovanie + msg: Šifrovanie nemôže byť prázdne. + text: Pre väčšinu serverov je SSL odporúčaná možnosť. + ssl: SSL + tls: TLS + none: Žiadne + smtp_port: + label: SMTP port + msg: Port SMTP musí byť číslo 1 ~ 65535. + text: Port na váš poštový server. + smtp_username: + label: SMTP username + msg: Používateľské meno SMTP nemôže byť prázdne. + smtp_password: + label: SMTP password + msg: Heslo SMTP nemôže byť prázdne. + test_email_recipient: + label: Test email recipients + text: Zadajte e-mailovú adresu, na ktorú sa budú odosielať testy. + msg: Príjemcovia testovacieho e-mailu sú neplatní + smtp_authentication: + label: Povoliť autentifikáciu + title: SMTP authentication + msg: Overenie SMTP nemôže byť prázdne. + "yes": "Áno" + "no": "Nie" + branding: + page_title: Budovanie značky + logo: + label: Logo + msg: Logo nemôže byť prázdne. + text: Obrázok loga v ľavej hornej časti vašej stránky. Použite široký obdĺžnikový obrázok s výškou 56 a pomerom strán väčším ako 3:1. Ak ho ponecháte prázdne, zobrazí sa text názvu stránky. + mobile_logo: + label: Mobile logo + text: Logo použité na mobilnej verzii vášho webu. Použite široký obdĺžnikový obrázok s výškou 56. Ak pole ponecháte prázdne, použije sa obrázok z nastavenia „logo“. + square_icon: + label: Square icon + msg: Ikona štvorca nemôže byť prázdna. + text: Obrázok použitý ako základ pre ikony metadát. V ideálnom prípade by mal byť väčšií ako 512 x 512. + favicon: + label: favicon + text: Favicon pre váš web. Ak chcete správne fungovať cez CDN, musí to byť png. Veľkosť sa zmení na 32 x 32. Ak zostane prázdne, použije sa „štvorcová ikona“. + legal: + page_title: Legálne + terms_of_service: + label: Terms of service + text: "Tu môžete pridať obsah zmluvných podmienok. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu." + privacy_policy: + label: Privacy policy + text: "Tu môžete pridať obsah zásad ochrany osobných údajov. Ak už máte dokument umiestnený inde, uveďte tu celú URL adresu." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Písať + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Každá nová otázka musí mať aspoň jedenu odporúčaciu značku." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: trvalý odkaz + text: Vlastné štruktúry URL môžu zlepšiť použiteľnosť a doprednú kompatibilitu vašich odkazov. + robots: + label: robots.txt + text: Toto natrvalo prepíše všetky nastavenia súvisiace so stránkou. + themes: + page_title: Témy + themes: + label: Témy + text: Vyberte existujúcu tému. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Upraviť farby používané vašími motívmi + css_and_html: + page_title: CSS a HTML + custom_css: + label: Vlastné CSS + text: > + + head: + label: Head + text: > + + header: + label: Hlavička + text: > + + footer: + label: Päta + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Prihlásenie + membership: + title: Členstvo + label: Povoliť nové registrácie + text: Vypnúť, aby sa zabránilo vytvorenie nového účtu hocikým. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Súkromné + label: Vyžaduje sa prihlásenie + text: Do tejto komunity majú prístup iba prihlásení používatelia + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar Base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (voliteľné) + empty: nemôže byť prázdne + invalid: je neplatné + btn_submit: Uložiť + not_found_props: "Požadovaná vlastnosť {{ key }} nebola nájdená." + select: Select + page_review: + review: Preskúmanie + proposed: navrhované + question_edit: Úprava otázky + answer_edit: Úprava odpovede + tag_edit: Úprava značky + edit_summary: Upraviť súhrn + edit_question: Upraviť otázku + edit_answer: Upraviť odpoveď + edit_tag: Upraviť značku + empty: Nezostali žiadne úlohy kontroly. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: zrušené zmazanie + deleted: vymazané + downvote: hlasovať proti + upvote: hlasovať za + accept: akceptované + cancelled: zrušené + commented: komentované + rollback: Návrat + edited: zmenené + answered: odpovedané + asked: spýtané + closed: uzavreté + reopened: znovu otvorené + created: vytvorené + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "História pre" + tag_title: "Časová os pre" + show_votes: "Zobraziť hlasy" + n_or_a: N/A + title_for_question: "Časová os pre" + title_for_answer: "Časová os odpovede na {{ title }} od {{ author }}" + title_for_tag: "Časová os pre značku" + datetime: Dátum a čas + type: Typ + by: Od + comment: Komentár + no_data: "Nič sa nám nepodarilo nájsť." + users: + title: Použivatelia + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Zamestnanci našej komunity + reputation: reputácia + votes: hlasy + prompt: + leave_page: Ste si istý, že chcete opustiť stránku? + changes_not_save: Vaše zmeny nemusia byť uložené. + draft: + discard_confirm: Naozaj chcete zahodiť svoj koncept? + messages: + post_deleted: Tento príspevok bol odstránený. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/sq_AL.yaml b/i18n/sq_AL.yaml new file mode 100644 index 000000000..c7bfcaa8f --- /dev/null +++ b/i18n/sq_AL.yaml @@ -0,0 +1,1371 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + role: + name: + user: + other: "User" + admin: + other: "Admin" + moderator: + other: "Moderator" + description: + user: + other: "Default with no special access." + admin: + other: "Have the full power to access the site." + moderator: + other: "Has access to all posts except admin settings." + email: + other: "Email" + password: + other: "Password" + email_or_password_wrong_error: + other: "Email and password do not match." + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: "Answer do not found." + cannot_deleted: + other: "No permission to delete." + cannot_update: + other: "No permission to update." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + cannot_deleted: + other: "No permission to delete." + cannot_close: + other: "No permission to close." + cannot_update: + other: "No permission to update." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + recommend_tag_not_found: + other: "Recommend Tag is not exist." + recommend_tag_enter: + other: "Please enter at least one required tag." + not_contain_synonym_tags: + other: "Should not contain synonym tags." + cannot_update: + other: "No permission to update." + cannot_set_synonym_as_itself: + other: "You cannot set the synonym of the current tag as itself." + smtp: + config_from_name_cannot_be_email: + other: "The From Name cannot be a email address." + theme: + not_found: + other: "Theme not found." + revision: + review_underway: + other: "Can't edit currently, there is a version in the review queue." + no_permission: + other: "No permission to Revision." + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + cannot_update_your_role: + other: "You cannot modify your role." + not_allowed_registration: + other: "Currently the site is not open for registration" + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + create_table_failed: + other: "Create table failed" + install: + create_config_failed: + other: "Can't create the config.yaml file." + report: + spam: + name: + other: "spam" + desc: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + desc: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + desc: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + desc: + other: "This post requires staff attention for another reason not listed above." + question: + close: + duplicate: + name: + other: "spam" + desc: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + desc: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + desc: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + desc: + other: "This post requires another reason not listed above." + operation_type: + asked: + other: "asked" + answered: + other: "answered" + modified: + other: "modified" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + accept_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: "After you've done that, click “Next” button." + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: display_name must be at 2 - 30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8 - 32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/sr_SP.yaml b/i18n/sr_SP.yaml new file mode 100644 index 000000000..094a05523 --- /dev/null +++ b/i18n/sr_SP.yaml @@ -0,0 +1,1384 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Unknown error. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + email: + other: Email + password: + other: Password + email_or_password_wrong_error: + other: Email and password do not match. + error: + admin: + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + question: + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + rank: + fail_to_meet_the_condition: + other: Rank fail to meet the condition. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend Tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The From Name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to Revision. + user: + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + report: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude: + name: + other: rude or abusive + desc: + other: A reasonable person would find this content inappropriate for respectful discourse. + duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + not_answer: + name: + other: not an answer + desc: + other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether. + not_need: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + other: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted +#The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4 MB. + desc: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: URL slug up to 35 characters. + desc: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comments + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your question is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to {{site_name}} + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to {{site_name}} + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location (optional) + placeholder: "City, Country" + notification: + heading: Notifications + email: + label: Email Notifications + radio: "Answers to your questions, comments, and more" + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + heading: Interface + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + reopen: + title: Reopen this post + content: Are you sure you want to reopen? + success: This post has been reopened + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + approve: Approve + reject: Reject + skip: Skip + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to {{site_name}} + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: "db:3306" + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_404: + desc: "Unfortunately, this page doesn't exist." + back_home: Back to homepage + page_50X: + desc: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + css-html: CSS/HTML + login: Login + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site Statistics + questions: "Questions:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + active_users: "Active users:" + flags: "Flags:" + site_health_status: Site Health Status + version: "Version:" + https: "HTTPS:" + uploading_files: "Uploading files:" + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System Info + storage_used: "Storage used:" + uptime: "Uptime:" + answer_links: Answer Links + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_desc: A normal user can ask and answer questions. + suspended_name: suspended + suspended_desc: A suspended user can't log in. + deleted_name: deleted + deleted_desc: "Delete profile, authentication associations." + inactive_name: inactive + inactive_desc: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: "Change {{ type }} status to..." + normal_name: normal + normal_desc: A normal post available to everyone. + closed_name: closed + closed_desc: "A closed question can't answer, but still can edit, vote and comment." + deleted_name: deleted + deleted_desc: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + display_name: + label: Display Name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site Description (optional) + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP Authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo (optional) + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square Icon (optional) + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of Service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy Policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + write: + page_title: Write + recommend_tags: + label: Recommend Tags + text: "Please input tag slug above, one tag per line." + required_tag: + title: Required Tag + label: Set recommend tag as required + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved Tags + text: "Reserved tags can only be added to a post by moderator." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + navbar_style: + label: Navbar Style + text: Select an existing theme. + primary_color: + label: Primary Color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: This will insert as + head: + label: Head + text: This will insert before + header: + label: Header + text: This will insert after + footer: + label: Footer + text: This will insert before . + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + form: + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores + users_with_the_most_vote: Users who voted the most + staffs: Our community staff + reputation: reputation + votes: votes diff --git a/i18n/sv_SE.yaml b/i18n/sv_SE.yaml new file mode 100644 index 000000000..6c0627613 --- /dev/null +++ b/i18n/sv_SE.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Success. + unknown: + other: Okänt fel. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + forbidden_error: + other: Förbjudet. + duplicate_request_error: + other: Dubblett inlämning. + action: + report: + other: Flag + edit: + other: Redigera + delete: + other: Radera + close: + other: Stäng + reopen: + other: Öppna igen + forbidden_error: + other: Förbjudet. + pin: + other: Fäst + hide: + other: Unlist + unpin: + other: Unpin + show: + other: Lista + invite_someone_to_answer: + other: Redigera + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: Användare + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Skapa ny tagg + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: Email + e_mail: + other: Email + password: + other: Lösenord + pass: + other: Lösenord + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Email and password do not match. + error: + common: + invalid_url: + other: Ogiltig URL. + status_invalid: + other: Ogiltig status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Din fråga har raderats + your_answer_was_deleted: + other: Ditt svar har raderats + your_comment_was_deleted: + other: Din kommentar har raderats + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Bekräfta din nya e-postadress" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] Ny fråga: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Bekräfta ditt nya konto" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: Prev + next: Nästa + page_title: + question: Fråga + questions: Frågor + tag: Tagg + tags: Taggar + tag_wiki: tag wiki + create_tag: Create Tag + edit_tag: Edit Tag + ask_a_question: Create Question + edit_question: Redigera fråga + edit_answer: Redigera svar + search: Sök + posts_containing: Posts containing + settings: Inställningar + notifications: Notifications + login: Logga in + sign_up: Registrera dig + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Användare + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Logga ut + notifications: + title: Notifications + inbox: Inkorg + achievement: Achievements + new_alerts: New alerts + all_read: Markera alla som lästa + show_more: Visa mer + someone: Someone + inbox_type: + all: Alla + posts: Inlägg + invites: Inbjudningar + votes: Röster + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Kontakta oss + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Kod + msg: + empty: Code cannot be empty. + language: + label: Språk + placeholder: Automatic detection + btn_cancel: Avbryt + btn_confirm: Lägg till + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Hjälp + hr: + text: Horizontal rule + image: + text: Bild + add_image: Lägg till bild + tab_image: Ladda upp bild + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Beskrivning + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Beskrivning + btn_cancel: Avbryt + btn_confirm: Lägg till + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Beskrivning + btn_cancel: Avbryt + btn_confirm: Lägg till + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Tabell + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Avbryt + btn_submit: Skicka + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Avbryt + btn_submit: Skicka + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Visningsnamn + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Beskrivning + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Avbryt + btn_submit: Skicka + btn_post: Post new tag + tag_info: + created_at: Skapad + edited_at: Edited + history: Historik + synonyms: + title: Synonymer + text: The following tags will be remapped to + empty: Inga synonymer hittades. + btn_add: Lägg till en synonym + btn_edit: Redigera + btn_save: Spara + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Stäng + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Lägg till tagg + btn_save_edits: Save edits + btn_cancel: Avbryt + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: nu + x_seconds_ago: "{{count}} s sedan" + x_minutes_ago: "{{count}} m sedan" + x_hours_ago: "{{count}} t sedan" + hour: timme + day: dag + hours: timmar + days: dagar + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Lägg till kommentar + reply_to: Reply to + btn_reply: Svara + btn_edit: Redigera + btn_delete: Radera + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Avbryt + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Avbryt + tags: + title: Tags + sort_buttons: + popular: Popular + name: Namn + newest: Newest + button_follow: Följ + button_following: Följer + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Användare + badges: Badges + profile: Profil + setting: Inställningar + logout: Logga ut + admin: Admin + review: Review + bookmark: Bokmärken + moderation: Moderation + search: + placeholder: Sök + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Ändra + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Logga in för att fortsätta + info_sign: Har du inget konto? <1>Registrera dig + info_login: Har du redan ett konto? <1>Logga in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Glömt lösenord? + name: + label: Namn + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: E-postadress + msg: + empty: Email cannot be empty. + password: + label: Lösenord + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Glömt ditt lösenord + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: E-postadress + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Avbryt + btn_update: Uppdatera e-postadress + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Ny e-postadress + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Uppdatera e-postadress + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Återställ mitt lösenord + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Lösenord + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Bekräfta nytt lösenord + settings: + page_title: Inställningar + goto_modify: Go to modify + nav: + profile: Profil + notification: Notifications + account: Konto + interface: Interface + profile: + heading: Profil + btn_name: Spara + display_name: + label: Visningsnamn + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Användarnamn + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profilbild + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: Du kan ladda upp din bild. + default: System + msg: Please upload an avatar + bio: + label: Om mig + website: + label: Webbplats + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "Stad, Land" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Konto + change_email_btn: Change email + change_pass_btn: Ändra lösenord + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Ny e-postadress + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Nuvarande lösenord + msg: Password cannot be empty. + password_title: Lösenord + current_pass: + label: Nuvarande lösenord + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: Nytt lösenord + pass_confirm: + label: Bekräfta nytt lösenord + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Bjud in personer som du tror kan svara. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Ditt svar + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Fortsätt + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Fäst + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Är du säker på att du vill radera? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Bekräfta + cancel: Avbryt + edit: Redigera + save: Spara + delete: Radera + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Logga in + signup: Registrera dig + logout: Logga ut + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Hoppa över + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Uppdatera + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Stäng + reopen: Reopen + ok: OK + light: Ljust + dark: Mörkt + system_setting: System setting + default: Standard + reset: Återställ + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignorera + submit: Skicka + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Sökresultat + keywords: Keywords + options: Alternativ + follow: Follow + following: Following + counts: "{{count}} resultat" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Dela + copy: Kopiera länk + via: Dela inlägg via... + copied: Copied + facebook: Dela på Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Din e-postadress har uppdaterats. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Redigera + save: Spara + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} frågor" + x_answers: "{{ count }} svar" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bokmärken + reputation: Reputation + comments: Kommentarer + votes: Röster + badges: Badges + newest: Newest + score: Score + edit_profile: Redigera profil + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: Om mig + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Nästa + done: Klar + config_yaml_error: Can't create the config.yaml file. + lang: + label: Välj ett språk + db_type: + label: Database engine + db_username: + label: Användarnamn + placeholder: root + msg: Username cannot be empty. + db_password: + label: Lösenord + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Databasnamn + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Skapa config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Privat + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Namn + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Lösenord + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Varning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Användare + badges: Badges + flags: Flags + settings: Inställningar + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Användarvillkor + privacy: Privacy + seo: SEO + customize: Anpassa + themes: Teman + login: Logga in + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Välkommen till {{site_name}} + user_center: + login: Login + qrcode_login_tip: Använd {{ agentName }} för att skanna QR-koden och logga in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Grattis + content: You've earned a new badge. + close: Stäng + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Frågor:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Svar:" + comments: "Kommentarer:" + votes: "Röster:" + users: "Användare:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Privat + public: Public + smtp: "SMTP:" + timezone: "Tidszon:" + system_info: System info + go_version: "Go version:" + database: "Databas:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Länkar + plugins: Plugins + github: GitHub + blog: Blogg + contact: Kontakt + forum: Forum + documents: Dokument + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Ja" + "no": "Nej" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Ändra användarroll till... + btn_cancel: Avbryt + btn_submit: Skicka + new_password_modal: + title: Set new password + form: + fields: + password: + label: Lösenord + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Avbryt + btn_submit: Skicka + edit_profile_modal: + title: Redigera profil + form: + fields: + display_name: + label: Visningsnamn + msg_range: Display name must be 2-30 characters in length. + username: + label: Användarnamn + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Ogiltig e-postadress. + edit_success: Edited successfully + btn_cancel: Avbryt + btn_submit: Skicka + user_modal: + title: Lägg till ny användare + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separera "namn, e-postadress, lösenord" med kommatecken. En användare per rad. + msg: "Please enter the user's email, one per line." + display_name: + label: Visningsnamn + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Lösenord + msg: Password must be at 8-32 characters in length. + btn_cancel: Avbryt + btn_submit: Skicka + users: + title: Användare + name: Namn + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Roll + action: Action + change: Ändra + all: Alla + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: Användare + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Ändra status + change_role: Ändra roll + show_logs: Visa loggar + add_user: Lägg till användare + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Ändra + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Ändra + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Ange en giltig URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Tidszon + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Kryptering + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: Ingen + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Aktivera autentisering + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Ja" + "no": "Nej" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Användarvillkor + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Integritetspolicy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalänk + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Teman + themes: + label: Teman + text: Select an existing theme. + color_scheme: + label: Färgschema + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS och HTML + custom_css: + label: Anpassad CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Medlemskap + label: Tillåt nya registreringar + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: Alla + active: Aktiv + inactive: Inaktiv + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Namn + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Aktivera + settings: Inställningar + settings_users: + title: Användare + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Tillåt användare att ändra sitt visningsnamn + allow_update_username: + label: Tillåt användare att ändra sitt användarnamn + allow_update_avatar: + label: Tillåt användare att ändra sin profilbild + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Aktiv + activate: Aktivera + all: Alla + awards: Awards + deactivate: Inaktivera + filter: + placeholder: Filter by name, badge:id + group: Grupp + inactive: Inaktiv + name: Namn + show_logs: Visa loggar + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Spara + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Redigera tagg + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Visa röster" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Användare + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: röster + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Dina ändringar kanske inte sparas. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/te_IN.yaml b/i18n/te_IN.yaml new file mode 100644 index 000000000..4b2629171 --- /dev/null +++ b/i18n/te_IN.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: విజయవంతమైంది. + unknown: + other: తెలియని సమస్య. + request_format_error: + other: Request format is not valid. + unauthorized_error: + other: Unauthorized. + database_error: + other: Data server error. + forbidden_error: + other: Forbidden. + duplicate_request_error: + other: Duplicate submission. + action: + report: + other: Flag + edit: + other: Edit + delete: + other: Delete + close: + other: Close + reopen: + other: Reopen + forbidden_error: + other: Forbidden. + pin: + other: Pin + hide: + other: Unlist + unpin: + other: Unpin + show: + other: List + invite_someone_to_answer: + other: Edit + undelete: + other: Undelete + merge: + other: Merge + role: + name: + user: + other: User + admin: + other: Admin + moderator: + other: Moderator + description: + user: + other: Default with no special access. + admin: + other: Have the full power to access the site. + moderator: + other: Has access to all posts except admin settings. + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: Write comment + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: Email + e_mail: + other: Email + password: + other: Password + pass: + other: Password + old_pass: + other: Current password + original_text: + other: This post + email_or_password_wrong_error: + other: Email and password do not match. + error: + common: + invalid_url: + other: Invalid URL. + status_invalid: + other: Invalid status. + password: + space_invalid: + other: Password cannot contain spaces. + admin: + cannot_update_their_password: + other: You cannot modify your password. + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: Email and password do not match. + answer: + not_found: + other: Answer do not found. + cannot_deleted: + other: No permission to delete. + cannot_update: + other: No permission to update. + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Comment are not allowed to edit. + not_found: + other: Comment not found. + cannot_edit_after_deadline: + other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email already exists. + need_to_be_verified: + other: Email should be verified. + verify_url_expired: + other: Email verified URL has expired, please resend the email. + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: Language file not found. + object: + captcha_verification_failed: + other: Captcha wrong. + disallow_follow: + other: You are not allowed to follow. + disallow_vote: + other: You are not allowed to vote. + disallow_vote_your_self: + other: You can't vote for your own post. + not_found: + other: Object not found. + verification_failed: + other: Verification failed. + email_or_password_incorrect: + other: Email and password do not match. + old_password_verification_failed: + other: The old password verification failed + new_password_same_as_previous_setting: + other: The new password is the same as the previous one. + already_deleted: + other: This post has been deleted. + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: Question not found. + cannot_deleted: + other: No permission to delete. + cannot_close: + other: No permission to close. + cannot_update: + other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: Report handle failed. + not_found: + other: Report not found. + tag: + already_exist: + other: Tag already exists. + not_found: + other: Tag not found. + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: Please enter at least one required tag. + not_contain_synonym_tags: + other: Should not contain synonym tags. + cannot_update: + other: No permission to update. + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: You cannot set the synonym of the current tag as itself. + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: Theme not found. + revision: + review_underway: + other: Can't edit currently, there is a version in the review queue. + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: Email and password do not match. + not_found: + other: User not found. + suspended: + other: User has been suspended. + username_invalid: + other: Username is invalid. + username_duplicate: + other: Username is already in use. + set_avatar: + other: Avatar set failed. + cannot_update_your_role: + other: You cannot modify your role. + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Read config failed + database: + connection_failed: + other: Database connection failed + create_table_failed: + other: Create table failed + install: + create_config_failed: + other: Can't create the config.yaml file. + upload: + unsupported_file_format: + other: Unsupported file format. + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: spam + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: needs close + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: spam + desc: + other: This question has been asked before and already has an answer. + guideline: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + multiple: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: something else + desc: + other: This post requires another reason not listed above. + operation_type: + asked: + other: asked + answered: + other: answered + modified: + other: modified + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: updated question + answer_the_question: + other: answered question + update_answer: + other: updated answer + accept_answer: + other: accepted answer + comment_question: + other: commented question + comment_answer: + other: commented answer + reply_to_you: + other: replied to you + mention_you: + other: mentioned you + your_question_is_closed: + other: Your question has been closed + your_question_was_deleted: + other: Your question has been deleted + your_answer_was_deleted: + other: Your answer has been deleted + your_comment_was_deleted: + other: Your comment has been deleted + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: accept + accepted: + other: accepted + edit: + other: edit + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + desc: >- + + pagination: + prev: మునుపటి + next: Next + page_title: + question: ప్రశ్న + questions: ప్రశ్నలు + tag: ట్యాగ్ + tags: టాగ్లు + tag_wiki: tag wiki + create_tag: ట్యాగ్‌ని సృష్టించండి + edit_tag: ట్యాగ్‌ని సవరించండి + ask_a_question: Create Question + edit_question: ప్రశ్నను సవరించండి + edit_answer: సమాధానాన్ని సవరించండి + search: Search + posts_containing: కలిగి ఉన్న పోస్ట్‌లు + settings: Settings + notifications: నోటిఫికేషన్‌లు + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: ఖాతా నిలిపివేయబడింది + admin: అడ్మిన్ + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Website Maintenance + users: Users + oauth_callback: Processing + http_404: HTTP Error 404 + http_50X: HTTP Error 500 + http_403: HTTP Error 403 + logout: Log Out + notifications: + title: నోటిఫికేషన్లు + inbox: ఇన్‌బాక్స్ + achievement: విజయాలు + new_alerts: New alerts + all_read: Mark all as read + show_more: Show more + someone: Someone + inbox_type: + all: All + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: Your Account has been Suspended + until_time: "Your account was suspended until {{ time }}." + forever: This user was suspended forever. + end: You don't meet a community guideline. + contact_us: Contact us + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image file + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed {{size}} MB. + desc: + label: Description + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: Table + heading: Heading + cell: Cell + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + desc: + label: Description + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: Cancel + btn_submit: Submit + btn_post: Post new tag + tag_info: + created_at: Created + edited_at: Edited + history: History + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: Are you sure you wish to delete? + close: Close + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + default_first_reason: Add tag + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: now + x_seconds_ago: "{{count}}s ago" + x_minutes_ago: "{{count}}m ago" + x_hours_ago: "{{count}}h ago" + hour: hour + day: day + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: "{{count}} more comments" + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. + tip_vote: It adds something useful to the post + edit_answer: + title: Edit Answer + default_reason: Edit answer + default_first_reason: Add answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + feedback: + characters: content must be at least 6 characters in length. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: Newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_desc: The tag has no description. + more: More + wiki: Wiki + ask: + title: Create Question + edit_title: Edit Question + default_reason: Edit question + default_first_reason: Create question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: What's your topic? Be specific. + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: "Describe what your content is about, at least one tag is required." + no_result: No tags matched + tag_required_text: Required tag (at least one) + header: + nav: + question: Questions + tag: Tags + user: Users + badges: Badges + profile: Profile + setting: Settings + logout: Log out + admin: Admin + review: Review + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: Search + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account. + info: "If it doesn't arrive, check your spam folder." + another: >- + We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: Log in to continue + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + agreements: By registering, you agree to the <1>privacy policy and <3>terms of service. + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly. + email: + label: New email + msg: + empty: Email cannot be empty. + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm new password + settings: + page_title: Settings + goto_modify: Go to modify + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + heading: Profile + btn_name: Save + display_name: + label: Display name + msg: Display name cannot be empty. + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username must be 2-30 characters in length. + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile image + gravatar: Gravatar + gravatar_text: You can change image on + custom: Custom + custom_text: You can upload your image. + default: System + msg: Please upload an avatar + bio: + label: About me + website: + label: Website + placeholder: "https://example.com" + msg: Website incorrect format + location: + label: Location + placeholder: "City, Country" + notification: + heading: Email Notifications + turn_on: Turn on + inbox: + label: Inbox notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: Account + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation instructions. + email: + label: Email + new_email: + label: New email + msg: New email cannot be empty. + pass: + label: Current password + msg: Password cannot be empty. + password_title: Password + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New password + pass_confirm: + label: Confirm new password + interface: + heading: Interface + lang: + label: Interface language + text: User interface language. It will change when you refresh the page. + my_logins: + title: My logins + label: Log in or sign up on this site using these accounts. + modal_title: Remove login + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + forbidden_operate_self: Forbidden to operate on yourself + review: Your revision will show after review. + sent_success: Sent successfully + related_question: + title: Related + answers: answers + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Select people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: Asked + asked: asked + update: Modified + edit: edited + commented: commented + Views: Viewed + Follow: Follow + Following: Following + follow_tip: Follow this question to receive notifications + answered: answered + closed_in: Closed in + show_exist: Show existing question. + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: Answers + score: Score + newest: Newest + oldest: Oldest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + edit_answer: Edit my existing answer + btn_name: Post your answer + add_another_answer: Add another answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + characters: content must be at least 6 characters in length. + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: Reopen this post + content: Are you sure you want to reopen? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_answer_deleted: This answer has been deleted + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: Confirm + cancel: Cancel + edit: Edit + save: Save + delete: Delete + undelete: Undelete + list: List + unlist: Unlist + unlisted: Unlisted + login: Log in + signup: Sign up + logout: Log out + verify: Verify + create: Create + approve: Approve + reject: Reject + skip: Skip + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: "{{count}} Results" + counts_loading: "... Results" + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: "<1>[tag] search with a tag" + user: "<1>user:username search by author" + answer: "<1>answers:0 unanswered questions" + score: "<1>score:3 posts with a 3+ score" + question: "<1>is:question search questions" + is_answer: "<1>is:answer search answers" + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was already changed? + unsubscribe: + page_title: Unsubscribe + success_title: Unsubscribe Successful + success_desc: You have been successfully removed from this subscriber list and won't receive any further emails from us. + link: Change settings + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: "{{ count }} Questions" + x_answers: "{{ count }} answers" + x_posts: "{{ count }} Posts" + questions: Questions + answers: Answers + newest: Newest + active: Active + hot: Hot + frequent: Frequent + recommend: Recommend + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + badges: Badges + newest: Newest + score: Score + edit_profile: Edit profile + visited_x_days: "Visited {{ count }} days" + viewed: Viewed + joined: Joined + comma: "," + last_login: Seen + about_me: About Me + about_me_empty: "// Hello, World !" + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + content_empty: No posts found. + accepted: Accepted + answered: answered + asked: asked + downvoted: downvoted + mod_short: MOD + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + recent_badges: Recent Badges + install: + title: Installation + next: Next + done: Done + config_yaml_error: Can't create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database host + placeholder: "db:3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: answer + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + desc: >- + You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it. + info: After you've done that, click "Next" button. + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: Email address of key contact responsible for this site. + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: Name + msg: Name cannot be empty. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure location. + msg: Password cannot be empty. + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your site is ready + ready_desc: >- + If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. + good_luck: "Have fun, and good luck!" + warn_title: Warning + warn_desc: >- + The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_desc: >- + You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: views + votes: votes + answers: answers + accepted: Accepted + page_error: + http_error: HTTP Error {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "We are under maintenance, we'll be back soon." + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + badges: Badges + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + branding: Branding + legal: Legal + write: Write + tos: Terms of Service + privacy: Privacy + seo: SEO + customize: Customize + themes: Themes + login: Login + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: Admin + dashboard: + title: Dashboard + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "Questions:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "Answers:" + comments: "Comments:" + votes: "Votes:" + users: "Users:" + flags: "Flags:" + reviews: "Reviews:" + site_health: Site health + version: "Version:" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "Timezone:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "Storage used:" + uptime: "Uptime:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: Documents + feedback: Feedback + support: Support + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + "yes": "Yes" + "no": "No" + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + writable: Writable + not_writable: Not writable + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + flagged_type: Flagged {{ type }} + created: Created + action: Action + review: Review + user_role_modal: + title: Change user role to... + btn_cancel: Cancel + btn_submit: Submit + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Status + role: Role + action: Action + change: Change + all: All + staff: Staff + more: More + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + Moderator: Moderator + Admin: Admin + User: User + filter: + placeholder: "Filter by name, user:id" + set_new_password: Set new password + edit_profile: Edit profile + change_status: Change status + change_role: Change role + show_logs: Show logs + add_user: Add user + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Questions + unlisted: Unlisted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + pending: Pending + filter: + placeholder: "Filter by title, question:id" + answers: + page_title: Answers + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: "Filter by title, answer:id" + general: + page_title: General + name: + label: Site name + msg: Site name cannot be empty. + text: "The name of this site, as used in the title tag." + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_desc: + label: Short site description + msg: Short site description cannot be empty. + text: "Short description, as used in the title tag on homepage." + desc: + label: Site description + msg: Site description cannot be empty. + text: "Describe this site in one sentence, as used in the meta description tag." + contact_email: + label: Contact email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: Interface + language: + label: Interface language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + tls: TLS + none: None + smtp_port: + label: SMTP port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test email recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: Enable authentication + title: SMTP authentication + msg: SMTP authentication cannot be empty. + "yes": "Yes" + "no": "No" + branding: + page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile logo + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used. + square_icon: + label: Square icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used. + legal: + page_title: Legal + terms_of_service: + label: Terms of service + text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here." + privacy_policy: + label: Privacy policy + text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Write + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "Every new question must have at least one recommend tag." + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: SEO + permalink: + label: Permalink + text: Custom URL structures can improve the usability, and forward-compatibility of your links. + robots: + label: robots.txt + text: This will permanently override any related site settings. + themes: + page_title: Themes + themes: + label: Themes + text: Select an existing theme. + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: Primary color + text: Modify the colors used by your themes + css_and_html: + page_title: CSS and HTML + custom_css: + label: Custom CSS + text: > + + head: + label: Head + text: > + + header: + label: Header + text: > + + footer: + label: Footer + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: Login + membership: + title: Membership + label: Allow new registrations + text: Turn off to prevent anyone from creating a new account. + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: Private + label: Login required + text: Only logged in users can access this community. + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (optional) + empty: cannot be empty + invalid: is invalid + btn_submit: Save + not_found_props: "Required property {{ key }} not found." + select: Select + page_review: + review: Review + proposed: proposed + question_edit: Question edit + answer_edit: Answer edit + tag_edit: Tag edit + edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag + empty: No review tasks left. + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: undeleted + deleted: deleted + downvote: downvote + upvote: upvote + accept: accept + cancelled: cancelled + commented: commented + rollback: rollback + edited: edited + answered: answered + asked: asked + closed: closed + reopened: reopened + created: created + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "History for" + tag_title: "Timeline for" + show_votes: "Show votes" + n_or_a: N/A + title_for_question: "Timeline for" + title_for_answer: "Timeline for answer to {{ title }} by {{ author }}" + title_for_tag: "Timeline for tag" + datetime: Datetime + type: Type + by: By + comment: Comment + no_data: "We couldn't find anything." + users: + title: Users + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: Our community staff + reputation: reputation + votes: votes + prompt: + leave_page: Are you sure you want to leave the page? + changes_not_save: Your changes may not be saved. + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/tr_TR.yaml b/i18n/tr_TR.yaml new file mode 100644 index 000000000..20a3284b8 --- /dev/null +++ b/i18n/tr_TR.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Başarılı. + unknown: + other: Bilinmeyen hata. + request_format_error: + other: İstek formatı geçerli değil. + unauthorized_error: + other: Yetkisiz erişim. + database_error: + other: Veri tabanı sunucu hatası. + forbidden_error: + other: Erişim engellendi. + duplicate_request_error: + other: Yinelenen gönderim. + action: + report: + other: Bildir + edit: + other: Düzenle + delete: + other: Sil + close: + other: Kapat + reopen: + other: Yeniden Aç + forbidden_error: + other: Erişim engellendi. + pin: + other: Sabitle + hide: + other: Listeden Kaldır + unpin: + other: Sabitlemeyi Kaldır + show: + other: Listele + invite_someone_to_answer: + other: Cevaplamaya Davet Et + undelete: + other: Silmeyi Geri Al + merge: + other: Birleştir + role: + name: + user: + other: Kullanıcı + admin: + other: Yönetici + moderator: + other: Moderatör + description: + user: + other: Özel erişimi olmayan varsayılan kullanıcı. + admin: + other: Siteye tam erişim gücüne sahiptir. + moderator: + other: Yönetici ayarları dışında tüm gönderilere erişebilir. + privilege: + level_1: + description: + other: Seviye 1 (özel takım, grup için daha az itibar gerektirir) + level_2: + description: + other: Seviye 2 (başlangıç topluluğu için düşük itibar gerektirir) + level_3: + description: + other: Seviye 3 (gelişmiş topluluk için yüksek itibar gerektirir) + level_custom: + description: + other: Özel Seviye + rank_question_add_label: + other: Soru sor + rank_answer_add_label: + other: Cevap yaz + rank_comment_add_label: + other: Yorum yaz + rank_report_add_label: + other: Bildir + rank_comment_vote_up_label: + other: Yorumu yukarı oyla + rank_link_url_limit_label: + other: Bir seferde 2'den fazla bağlantı paylaş + rank_question_vote_up_label: + other: Soruyu yukarı oyla + rank_answer_vote_up_label: + other: Cevabı yukarı oyla + rank_question_vote_down_label: + other: Soruyu aşağı oyla + rank_answer_vote_down_label: + other: Cevabı aşağı oyla + rank_invite_someone_to_answer_label: + other: Birini cevaplamaya davet et + rank_tag_add_label: + other: Yeni etiket oluştur + rank_tag_edit_label: + other: Etiket açıklamasını düzenle (inceleme gerekli) + rank_question_edit_label: + other: Başkasının sorusunu düzenle (inceleme gerekli) + rank_answer_edit_label: + other: Başkasının cevabını düzenle (inceleme gerekli) + rank_question_edit_without_review_label: + other: Başkasının sorusunu inceleme olmadan düzenle + rank_answer_edit_without_review_label: + other: Başkasının cevabını inceleme olmadan düzenle + rank_question_audit_label: + other: Soru düzenlemelerini incele + rank_answer_audit_label: + other: Cevap düzenlemelerini incele + rank_tag_audit_label: + other: Etiket düzenlemelerini incele + rank_tag_edit_without_review_label: + other: Etiket açıklamasını inceleme olmadan düzenle + rank_tag_synonym_label: + other: Etiket eş anlamlılarını yönet + email: + other: E-posta + e_mail: + other: E-posta + password: + other: Parola + pass: + other: Parola + old_pass: + other: Mevcut parola + original_text: + other: Bu gönderi + email_or_password_wrong_error: + other: E-posta ve parola eşleşmiyor. + error: + common: + invalid_url: + other: Geçersiz URL. + status_invalid: + other: Geçersiz durum. + password: + space_invalid: + other: Parola boşluk içeremez. + admin: + cannot_update_their_password: + other: Parolanızı değiştiremezsiniz. + cannot_edit_their_profile: + other: Profilinizi değiştiremezsiniz. + cannot_modify_self_status: + other: Durumunuzu değiştiremezsiniz. + email_or_password_wrong: + other: E-posta ve parola eşleşmiyor. + answer: + not_found: + other: Cevap bulunamadı. + cannot_deleted: + other: Silme izni yok. + cannot_update: + other: Güncelleme izni yok. + question_closed_cannot_add: + other: Sorular kapatıldı ve cevap eklenemez. + content_cannot_empty: + other: Cevap içeriği boş olamaz. + comment: + edit_without_permission: + other: Yorumları düzenleme izniniz yok. + not_found: + other: Yorum bulunamadı. + cannot_edit_after_deadline: + other: Yorum süresi çok uzun olduğu için artık düzenlenemez. + content_cannot_empty: + other: Yorum içeriği boş olamaz. + email: + duplicate: + other: Bu e-posta adresi zaten kullanılmaktadır. + need_to_be_verified: + other: E-posta doğrulanmalıdır. + verify_url_expired: + other: E-posta doğrulama URL'sinin süresi dolmuş, lütfen e-postayı yeniden gönderin. + illegal_email_domain_error: + other: Bu e-posta alan adına izin verilmiyor. Lütfen başka bir e-posta kullanın. + lang: + not_found: + other: Dil dosyası bulunamadı. + object: + captcha_verification_failed: + other: Captcha yanlış. + disallow_follow: + other: Takip etme izniniz yok. + disallow_vote: + other: Oy verme izniniz yok. + disallow_vote_your_self: + other: Kendi gönderinize oy veremezsiniz. + not_found: + other: Nesne bulunamadı. + verification_failed: + other: Doğrulama başarısız. + email_or_password_incorrect: + other: E-posta ve parola eşleşmiyor. + old_password_verification_failed: + other: Eski parola doğrulaması başarısız oldu. + new_password_same_as_previous_setting: + other: Yeni parola öncekiyle aynı. + already_deleted: + other: Bu gönderi silinmiş. + meta: + object_not_found: + other: Meta nesnesi bulunamadı. + question: + already_deleted: + other: Bu gönderi silinmiş. + under_review: + other: Gönderiniz inceleme bekliyor. Onaylandıktan sonra görünür olacaktır. + not_found: + other: Soru bulunamadı. + cannot_deleted: + other: Silme izni yok. + cannot_close: + other: Kapatma izni yok. + cannot_update: + other: Güncelleme izni yok. + content_cannot_empty: + other: İçerik boş olamaz. + rank: + fail_to_meet_the_condition: + other: İtibar seviyesi koşulu karşılamıyor. + vote_fail_to_meet_the_condition: + other: Geri bildiriminiz için teşekkürler. Oy kullanmak için en az {{.Rank}} itibara ihtiyacınız var. + no_enough_rank_to_operate: + other: Bu işlemi yapmak için en az {{.Rank}} itibara ihtiyacınız var. + report: + handle_failed: + other: Rapor işleme başarısız. + not_found: + other: Rapor bulunamadı. + tag: + already_exist: + other: Etiket zaten var. + not_found: + other: Etiket bulunamadı. + recommend_tag_not_found: + other: Önerilen etiket mevcut değil. + recommend_tag_enter: + other: Lütfen en az bir adet gerekli etiket giriniz. + not_contain_synonym_tags: + other: Eş anlamlı etiketler içermemelidir. + cannot_update: + other: Güncelleme izni yok. + is_used_cannot_delete: + other: Kullanımda olan bir etiketi silemezsiniz. + cannot_set_synonym_as_itself: + other: Bir etiketin eş anlamlısını kendisi olarak ayarlayamazsınız. + smtp: + config_from_name_cannot_be_email: + other: Gönderen adı bir e-posta adresi olamaz. + theme: + not_found: + other: Tema bulunamadı. + revision: + review_underway: + other: Şu anda düzenlenemez, inceleme kuyruğunda bir sürüm var. + no_permission: + other: Düzenleme izniniz yok. + user: + external_login_missing_user_id: + other: Üçüncü taraf platform benzersiz bir Kullanıcı ID'si sağlamıyor, bu nedenle giriş yapamazsınız. Lütfen site yöneticisiyle iletişime geçin. + external_login_unbinding_forbidden: + other: Bu girişi kaldırmadan önce lütfen hesabınız için bir giriş parolası ayarlayın. + email_or_password_wrong: + other: + other: E-posta ve parola eşleşmiyor. + not_found: + other: Kullanıcı bulunamadı. + suspended: + other: Kullanıcı askıya alındı. + username_invalid: + other: Kullanıcı adı geçersiz. + username_duplicate: + other: Kullanıcı adı zaten kullanımda. + set_avatar: + other: Avatar ayarlama başarısız. + cannot_update_your_role: + other: Kendi rolünüzü değiştiremezsiniz. + not_allowed_registration: + other: Şu anda site kayıt için açık değil. + not_allowed_login_via_password: + other: Şu anda site parola ile giriş yapmaya izin vermiyor. + access_denied: + other: Erişim reddedildi. + page_access_denied: + other: Bu sayfaya erişim izniniz yok. + add_bulk_users_format_error: + other: "{{.Line}} satırındaki '{{.Content}}' içeriğinde {{.Field}} biçimi hatası. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Bir kerede eklediğiniz kullanıcı sayısı 1-{{.MaxAmount}} aralığında olmalıdır." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Yapılandırma okunamadı. + database: + connection_failed: + other: Veritabanı bağlantısı başarısız. + create_table_failed: + other: Tablo oluşturma başarısız. + install: + create_config_failed: + other: config.yaml dosyası oluşturulamıyor. + upload: + unsupported_file_format: + other: Desteklenmeyen dosya formatı. + site_info: + config_not_found: + other: Site yapılandırması bulunamadı. + badge: + object_not_found: + other: Rozet nesnesi bulunamadı. + reason: + spam: + name: + other: spam + desc: + other: Bu gönderi bir reklam veya vandalizm. Mevcut konuyla ilgili veya yararlı değil. + rude_or_abusive: + name: + other: kaba veya taciz edici + desc: + other: "Makul bir kişi bu içeriği saygılı bir iletişim için uygunsuz bulurdu." + a_duplicate: + name: + other: kopya + desc: + other: Bu soru daha önce sorulmuş ve cevaplandırılmış. + placeholder: + other: Mevcut soru bağlantısını girin + not_a_answer: + name: + other: cevap değil + desc: + other: "Bu bir cevap olarak gönderilmiş, ancak soruyu cevaplamaya çalışmıyor. Bir düzenleme, yorum, başka bir soru olabilir veya tamamen silinmesi gerekebilir." + no_longer_needed: + name: + other: artık gerekli değil + desc: + other: Bu yorum güncelliğini yitirmiş, sohbet niteliğinde veya bu gönderiyle ilgili değil. + something: + name: + other: başka bir şey + desc: + other: Bu gönderi, yukarıda listelenmeyen başka bir nedenden dolayı personel ilgisi gerektiriyor. + placeholder: + other: Endişelerinizin ne olduğunu spesifik olarak belirtin + community_specific: + name: + other: topluluk kurallarına aykırı + desc: + other: Bu soru bir topluluk kılavuzuna uymuyor. + not_clarity: + name: + other: detay veya açıklık gerekiyor + desc: + other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı. + looks_ok: + name: + other: iyi görünüyor + desc: + other: Bu gönderi olduğu gibi iyi ve düşük kaliteli değil. + needs_edit: + name: + other: düzenleme gerektiriyor ve ben yaptım + desc: + other: Bu gönderideki sorunları kendiniz düzeltin ve iyileştirin. + needs_close: + name: + other: kapatılması gerekiyor + desc: + other: Kapatılmış bir soru cevaplanamaz, ancak düzenlenebilir, oylanabilir ve yorum yapılabilir. + needs_delete: + name: + other: silinmesi gerekiyor + desc: + other: Bu gönderi silinecek. + question: + close: + duplicate: + name: + other: kopya + desc: + other: Bu soru daha önce sorulmuş ve cevaplandırılmış. + guideline: + name: + other: topluluk kurallarına aykırı + desc: + other: Bu soru bir topluluk kılavuzuna uymuyor. + multiple: + name: + other: detay veya açıklık gerekiyor + desc: + other: Bu soru şu anda tek soruda birden fazla soru içeriyor. Sadece tek bir soruna odaklanmalı. + other: + name: + other: başka bir şey + desc: + other: Bu gönderi yukarıda listelenmeyen başka bir neden gerektiriyor. + operation_type: + asked: + other: soruldu + answered: + other: cevaplandı + modified: + other: değiştirildi + deleted_title: + other: Silinmiş soru + questions_title: + other: Sorular + tag: + tags_title: + other: Etiketler + no_description: + other: Bu etiketin açıklaması yok. + notification: + action: + update_question: + other: soruyu güncelledi + answer_the_question: + other: soruyu cevapladı + update_answer: + other: cevabı güncelledi + accept_answer: + other: cevabı kabul etti + comment_question: + other: soruya yorum yaptı + comment_answer: + other: cevaba yorum yaptı + reply_to_you: + other: size yanıt verdi + mention_you: + other: sizden bahsetti + your_question_is_closed: + other: Sorunuz kapatıldı + your_question_was_deleted: + other: Sorunuz silindi + your_answer_was_deleted: + other: Cevabınız silindi + your_comment_was_deleted: + other: Yorumunuz silindi + up_voted_question: + other: soruyu yukarı oyladı + down_voted_question: + other: soruyu aşağı oyladı + up_voted_answer: + other: cevabı yukarı oyladı + down_voted_answer: + other: cevabı aşağı oyladı + up_voted_comment: + other: yorumu yukarı oyladı + invited_you_to_answer: + other: sizi cevaplamaya davet etti + earned_badge: + other: Rozet kazandınız "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Yeni e-posta adresinizi onaylayın" + body: + other: "{{.SiteName}} için aşağıdaki bağlantıya tıklayarak yeni e-posta adresinizi onaylayın:
\n{{.ChangeEmailUrl}}

\n\nEğer bu değişikliği siz talep etmediyseniz, lütfen bu e-postayı dikkate almayın.

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} sorunuzu cevapladı" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} sizi cevaplamaya davet etti" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Cevabı biliyor olabileceğinizi düşünüyorum.

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} gönderinize yorum yaptı" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n{{.SiteName}} üzerinde görüntüle

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" + new_question: + title: + other: "[{{.SiteName}}] Yeni soru: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir.

\n\nAbonelikten çık" + pass_reset: + title: + other: "[{{.SiteName }}] Parola sıfırlama" + body: + other: "Birisi {{.SiteName}} üzerindeki parolanızı sıfırlamak istedi.

\n\nEğer bu siz değilseniz, bu e-postayı güvenle görmezden gelebilirsiniz.

\n\nYeni bir parola seçmek için aşağıdaki bağlantıya tıklayın:
\n{{.PassResetUrl}}\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." + register: + title: + other: "[{{.SiteName}}] Yeni hesabınızı onaylayın" + body: + other: "{{.SiteName}} sitesine hoş geldiniz!

\n\nYeni hesabınızı onaylamak ve etkinleştirmek için aşağıdaki bağlantıya tıklayın:
\n{{.RegisterUrl}}

\n\nYukarıdaki bağlantı tıklanabilir değilse, web tarayıcınızın adres çubuğuna kopyalayıp yapıştırmayı deneyin.\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." + test: + title: + other: "[{{.SiteName}}] Test E-postası" + body: + other: "Bu bir test e-postasıdır.\n

\n\n--
\nNot: Bu otomatik bir sistem e-postasıdır, lütfen bu mesaja yanıt vermeyin çünkü cevabınız görülmeyecektir." + action_activity_type: + upvote: + other: yukarı oyla + upvoted: + other: yukarı oyladı + downvote: + other: aşağı oyla + downvoted: + other: aşağı oyladı + accept: + other: kabul et + accepted: + other: kabul edildi + edit: + other: düzenle + review: + queued_post: + other: Sıradaki gönderi + flagged_post: + other: Bildirilen gönderi + suggested_post_edit: + other: Önerilen düzenlemeler + reaction: + tooltip: + other: "{{ .Names }} ve {{ .Count }} kişi daha..." + badge: + default_badges: + autobiographer: + name: + other: Otobiyografi Yazarı + desc: + other: Profil bilgilerini doldurdu. + certified: + name: + other: Sertifikalı + desc: + other: Yeni kullanıcı eğitimimizi tamamladı. + editor: + name: + other: Editör + desc: + other: İlk gönderi düzenlemesi. + first_flag: + name: + other: İlk Bildirim + desc: + other: İlk kez bir gönderiyi bildirdi. + first_upvote: + name: + other: İlk Yukarı Oylama + desc: + other: İlk kez bir gönderiyi yukarı oyladı. + first_link: + name: + other: İlk Bağlantı + desc: + other: İlk kez başka bir gönderiye bağlantı ekledi. + first_reaction: + name: + other: İlk Tepki + desc: + other: İlk kez bir gönderiye tepki verdi. + first_share: + name: + other: İlk Paylaşım + desc: + other: İlk kez bir gönderi paylaştı. + scholar: + name: + other: Bilim İnsanı + desc: + other: Bir soru sordu ve bir cevabı kabul etti. + commentator: + name: + other: Yorumcu + desc: + other: 5 yorum bıraktı. + new_user_of_the_month: + name: + other: Ayın Yeni Kullanıcısı + desc: + other: İlk aylarında üstün katkılarda bulundu. + read_guidelines: + name: + other: Kuralları Okuyan + desc: + other: '[Topluluk kurallarını] oku.' + reader: + name: + other: Okuyucu + desc: + other: 10'dan fazla cevap içeren bir konudaki tüm cevapları okudu. + welcome: + name: + other: Hoş Geldin + desc: + other: Bir yukarı oy aldı. + nice_share: + name: + other: Güzel Paylaşım + desc: + other: 25 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. + good_share: + name: + other: İyi Paylaşım + desc: + other: 300 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. + great_share: + name: + other: Harika Paylaşım + desc: + other: 1000 farklı ziyaretçi tarafından görüntülenen bir gönderi paylaştı. + out_of_love: + name: + other: Sevgiden + desc: + other: Bir günde 50 yukarı oy kullandı. + higher_love: + name: + other: Yüksek Sevgi + desc: + other: Bir günde 50 yukarı oyu 5 kez kullandı. + crazy_in_love: + name: + other: Çılgınca Aşık + desc: + other: Bir günde 50 yukarı oyu 20 kez kullandı. + promoter: + name: + other: Destekçi + desc: + other: Bir kullanıcıyı davet etti. + campaigner: + name: + other: Kampanyacı + desc: + other: 3 temel kullanıcıyı davet etti. + champion: + name: + other: Şampiyon + desc: + other: 5 üye davet etti. + thank_you: + name: + other: Teşekkürler + desc: + other: 20 yukarı oy aldı ve 10 yukarı oy verdi. + gives_back: + name: + other: Karşılık Veren + desc: + other: 100 yukarı oy aldı ve 100 yukarı oy verdi. + empathetic: + name: + other: Empatik + desc: + other: 500 yukarı oy aldı ve 1000 yukarı oy verdi. + enthusiast: + name: + other: Hevesli + desc: + other: 10 gün üst üste ziyaret etti. + aficionado: + name: + other: Meraklı + desc: + other: 100 gün üst üste ziyaret etti. + devotee: + name: + other: Hayran + desc: + other: 365 gün üst üste ziyaret etti. + anniversary: + name: + other: Yıldönümü + desc: + other: Bir yıl boyunca aktif üye, en az bir gönderi paylaştı. + appreciated: + name: + other: Takdir Edilen + desc: + other: 20 gönderisinde 1 yukarı oy aldı. + respected: + name: + other: Saygın + desc: + other: 100 gönderisinde 2 yukarı oy aldı. + admired: + name: + other: Hayranlık Uyandıran + desc: + other: 300 gönderisinde 5 yukarı oy aldı. + solved: + name: + other: Çözüldü + desc: + other: Bir cevabı kabul edildi. + guidance_counsellor: + name: + other: Rehber Danışman + desc: + other: 10 cevabı kabul edildi. + know_it_all: + name: + other: Her Şeyi Bilen + desc: + other: 50 cevabı kabul edildi. + solution_institution: + name: + other: Çözüm Kurumu + desc: + other: 150 cevabı kabul edildi. + nice_answer: + name: + other: Güzel Cevap + desc: + other: 10 veya daha fazla cevap puanı. + good_answer: + name: + other: İyi Cevap + desc: + other: 25 veya daha fazla cevap puanı. + great_answer: + name: + other: Harika Cevap + desc: + other: 50 veya daha fazla cevap puanı. + nice_question: + name: + other: Güzel Soru + desc: + other: 10 veya daha fazla soru puanı. + good_question: + name: + other: İyi Soru + desc: + other: 25 veya daha fazla soru puanı. + great_question: + name: + other: Harika Soru + desc: + other: 50 veya daha fazla soru puanı. + popular_question: + name: + other: Popüler Soru + desc: + other: 500 görüntülenme alan soru. + notable_question: + name: + other: Dikkat Çeken Soru + desc: + other: 1.000 görüntülenme alan soru. + famous_question: + name: + other: Ünlü Soru + desc: + other: 5.000 görüntülenme alan soru. + popular_link: + name: + other: Popüler Bağlantı + desc: + other: 50 tıklama alan harici bir bağlantı paylaştı. + hot_link: + name: + other: Sıcak Bağlantı + desc: + other: 300 tıklama alan harici bir bağlantı paylaştı. + famous_link: + name: + other: Ünlü Bağlantı + desc: + other: 100 tıklama alan harici bir bağlantı paylaştı. + default_badge_groups: + getting_started: + name: + other: Başlangıç + community: + name: + other: Topluluk + posting: + name: + other: Gönderi Yazmak +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Nasıl Biçimlendirilir + desc: >- + + pagination: + prev: Önceki + next: Sonraki + page_title: + question: Soru + questions: Sorular + tag: Etiket + tags: Etiketler + tag_wiki: etiket wikisi + create_tag: Etiket Oluştur + edit_tag: Etiketi Düzenle + ask_a_question: Create Question + edit_question: Soruyu Düzenle + edit_answer: Cevabı Düzenle + search: Ara + posts_containing: İçeren gönderiler + settings: Ayarlar + notifications: Bildirimler + login: Giriş Yap + sign_up: Kayıt Ol + account_recovery: Hesap Kurtarma + account_activation: Hesap Aktivasyonu + confirm_email: E-posta Onayı + account_suspended: Hesap Askıya Alındı + admin: Yönetici + change_email: E-posta Değiştir + install: Answer Kurulumu + upgrade: Answer Yükseltme + maintenance: Website Bakımı + users: Kullanıcılar + oauth_callback: İşleniyor + http_404: HTTP Hatası 404 + http_50X: HTTP Hatası 500 + http_403: HTTP Hatası 403 + logout: Çıkış Yap + notifications: + title: Bildirimler + inbox: Gelen Kutusu + achievement: Başarılar + new_alerts: Yeni uyarılar + all_read: Tümünü okundu olarak işaretle + show_more: Daha fazla göster + someone: Birisi + inbox_type: + all: Tümü + posts: Gönderiler + invites: Davetler + votes: Oylar + answer: Cevap + question: Soru + badge_award: Rozet + suspended: + title: Hesabınız Askıya Alındı + until_time: "Hesabınız {{ time }} tarihine kadar askıya alındı." + forever: Bu kullanıcı süresiz olarak askıya alındı. + end: Topluluk kurallarını karşılamıyorsunuz. + contact_us: Bize ulaşın + editor: + blockquote: + text: Alıntı + bold: + text: Kalın + chart: + text: Grafik + flow_chart: Akış şeması + sequence_diagram: Sıralama diyagramı + class_diagram: Sınıf diyagramı + state_diagram: Durum diyagramı + entity_relationship_diagram: Varlık ilişki diyagramı + user_defined_diagram: Kullanıcı tanımlı diyagram + gantt_chart: Gantt şeması + pie_chart: Pasta grafiği + code: + text: Kod Örneği + add_code: Kod örneği ekle + form: + fields: + code: + label: Kod + msg: + empty: Kod boş olamaz. + language: + label: Dil + placeholder: Otomatik algılama + btn_cancel: İptal + btn_confirm: Ekle + formula: + text: Formül + options: + inline: Satır içi formül + block: Blok formül + heading: + text: Başlık + options: + h1: Başlık 1 + h2: Başlık 2 + h3: Başlık 3 + h4: Başlık 4 + h5: Başlık 5 + h6: Başlık 6 + help: + text: Yardım + hr: + text: Yatay çizgi + image: + text: Resim + add_image: Resim ekle + tab_image: Resim Yükle + form_image: + fields: + file: + label: Resim dosyası + btn: Resim seç + msg: + empty: Dosya boş olamaz. + only_image: Sadece resim dosyalarına izin verilir. + max_size: Dosya boyutu {{size}} MB'ı geçemez. + desc: + label: Açıklama + tab_url: Resim URL'si + form_url: + fields: + url: + label: Resim URL'si + msg: + empty: Resim URL'si boş olamaz. + name: + label: Açıklama + btn_cancel: İptal + btn_confirm: Ekle + uploading: Yükleniyor + indent: + text: Girinti + outdent: + text: Girintiyi azalt + italic: + text: İtalik + link: + text: Bağlantı + add_link: Bağlantı ekle + form: + fields: + url: + label: URL + msg: + empty: URL boş olamaz. + name: + label: Açıklama + btn_cancel: İptal + btn_confirm: Ekle + ordered_list: + text: Numaralı liste + unordered_list: + text: Madde işaretli liste + table: + text: Tablo + heading: Başlık + cell: Hücre + file: + text: Dosya ekle + not_supported: "Bu dosya türü desteklenmiyor. {{file_type}} ile tekrar deneyin." + max_size: "Eklenen dosyaların boyutu {{size}} MB'ı geçemez." + close_modal: + title: Bu gönderiyi kapatıyorum çünkü... + btn_cancel: İptal + btn_submit: Gönder + remark: + empty: Boş olamaz. + msg: + empty: Lütfen bir neden seçin. + report_modal: + flag_title: Bu gönderiyi şu nedenle bildiriyorum... + close_title: Bu gönderiyi şu nedenle kapatıyorum... + review_question_title: Soruyu incele + review_answer_title: Cevabı incele + review_comment_title: Yorumu incele + btn_cancel: İptal + btn_submit: Gönder + remark: + empty: Boş olamaz. + msg: + empty: Lütfen bir neden seçin. + not_a_url: URL formatı yanlış. + url_not_match: URL kaynağı mevcut web sitesiyle eşleşmiyor. + tag_modal: + title: Yeni etiket oluştur + form: + fields: + display_name: + label: Görünen ad + msg: + empty: Görünen ad boş olamaz. + range: Görünen ad en fazla 35 karakter olabilir. + slug_name: + label: URL kısaltması + desc: URL kısaltması en fazla 35 karakter olabilir. + msg: + empty: URL kısaltması boş olamaz. + range: URL kısaltması en fazla 35 karakter olabilir. + character: URL kısaltması izin verilmeyen karakter içeriyor. + desc: + label: Açıklama + revision: + label: Revizyon + edit_summary: + label: Düzenleme özeti + placeholder: >- + Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi) + btn_cancel: İptal + btn_submit: Gönder + btn_post: Yeni etiket gönder + tag_info: + created_at: Oluşturuldu + edited_at: Düzenlendi + history: Geçmiş + synonyms: + title: Eş Anlamlılar + text: Aşağıdaki etiketler şuna yeniden eşlenecek + empty: Eş anlamlı bulunamadı. + btn_add: Eş anlamlı ekle + btn_edit: Düzenle + btn_save: Kaydet + synonyms_text: Aşağıdaki etiketler şuna yeniden eşlenecek + delete: + title: Bu etiketi sil + tip_with_posts: >- +

Gönderileri olan etiketin silinmesine izin vermiyoruz.

Lütfen önce bu etiketi gönderilerden kaldırın.

+ tip_with_synonyms: >- +

Eş anlamlıları olan etiketin silinmesine izin vermiyoruz.

Lütfen önce bu etiketin eş anlamlılarını kaldırın.

+ tip: Silmek istediğinizden emin misiniz? + close: Kapat + merge: + title: Etiket birleştir + source_tag_title: Kaynak etiket + source_tag_description: Kaynak etiket ve ilişkili verileri hedef etikete yeniden eşlenecek. + target_tag_title: Hedef etiket + target_tag_description: Birleştirmeden sonra bu iki etiket arasında bir eş anlamlı ilişkisi oluşturulacak. + no_results: Eşleşen etiket bulunamadı + btn_submit: Gönder + btn_close: Kapat + edit_tag: + title: Etiketi Düzenle + default_reason: Etiketi düzenle + default_first_reason: Etiket ekle + btn_save_edits: Düzenlemeleri kaydet + btn_cancel: İptal + dates: + long_date: D MMM + long_date_with_year: "D MMM, YYYY" + long_date_with_time: "D MMM, YYYY [saat] HH:mm" + now: şimdi + x_seconds_ago: "{{count}} saniye önce" + x_minutes_ago: "{{count}} dakika önce" + x_hours_ago: "{{count}} saat önce" + hour: saat + day: gün + hours: saatler + days: günler + month: month + months: months + year: year + reaction: + heart: kalp + smile: gülümseme + frown: üzgün + btn_label: tepki ekle veya kaldır + undo_emoji: '{{emoji}} tepkisini geri al' + react_emoji: '{{emoji}} ile tepki ver' + unreact_emoji: '{{emoji}} tepkisini kaldır' + comment: + btn_add_comment: Yorum ekle + reply_to: Yanıtla + btn_reply: Yanıtla + btn_edit: Düzenle + btn_delete: Sil + btn_flag: Bildir + btn_save_edits: Düzenlemeleri kaydet + btn_cancel: İptal + show_more: "{{count}} daha fazla yorum" + tip_question: >- + Daha fazla bilgi istemek veya iyileştirmeler önermek için yorumları kullanın. Yorumlarda soruları cevaplamaktan kaçının. + tip_answer: >- + Diğer kullanıcılara yanıt vermek veya onları değişikliklerden haberdar etmek için yorumları kullanın. Yeni bilgi ekliyorsanız, yorum yapmak yerine gönderinizi düzenleyin. + tip_vote: Gönderiye faydalı bir şey ekliyor + edit_answer: + title: Cevabı Düzenle + default_reason: Cevabı düzenle + default_first_reason: Cevap ekle + form: + fields: + revision: + label: Revizyon + answer: + label: Cevap + feedback: + characters: içerik en az 6 karakter uzunluğunda olmalıdır. + edit_summary: + label: Düzenleme özeti + placeholder: >- + Yaptığınız değişiklikleri kısaca açıklayın (düzeltilmiş yazım geliştirilmiş biçimlendirme) + btn_save_edits: Düzenlemeleri kaydet + btn_cancel: İptal + tags: + title: Etiketler + sort_buttons: + popular: Popüler + name: İsim + newest: En Yeni + button_follow: Takip Et + button_following: Takip Ediliyor + tag_label: sorular + search_placeholder: Etiket adına göre filtrele + no_desc: Bu etiketin açıklaması yok. + more: Daha Fazla + wiki: Wiki + ask: + title: Create Question + edit_title: Soruyu Düzenle + default_reason: Soruyu düzenle + default_first_reason: Create question + similar_questions: Benzer sorular + form: + fields: + revision: + label: Revizyon + title: + label: Başlık + placeholder: What's your topic? Be specific. + msg: + empty: Başlık boş olamaz. + range: Başlık en fazla 150 karakter olabilir + body: + label: İçerik + msg: + empty: İçerik boş olamaz. + tags: + label: Etiketler + msg: + empty: Etiketler boş olamaz. + answer: + label: Cevap + msg: + empty: Cevap boş olamaz. + edit_summary: + label: Düzenleme özeti + placeholder: >- + Değişikliklerinizi kısaca açıklayın (yazım hatası düzeltildi, dilbilgisi düzeltildi, biçimlendirme geliştirildi) + btn_post_question: Sorunuzu gönderin + btn_save_edits: Düzenlemeleri kaydet + answer_question: Kendi sorunuzu cevaplayın + post_question&answer: Sorunuzu ve cevabınızı gönderin + tag_selector: + add_btn: Etiket ekle + create_btn: Yeni etiket oluştur + search_tag: Etiket ara + hint: "Describe what your content is about, at least one tag is required." + no_result: Eşleşen etiket bulunamadı + tag_required_text: Gerekli etiket (en az bir tane) + header: + nav: + question: Sorular + tag: Etiketler + user: Kullanıcılar + badges: Rozetler + profile: Profil + setting: Ayarlar + logout: Çıkış yap + admin: Yönetici + review: İnceleme + bookmark: Yer İşaretleri + moderation: Moderasyon + search: + placeholder: Ara + footer: + build_on: >- + <1>Apache Answer tarafından desteklenmektedir - S&C topluluklarına güç veren açık kaynaklı yazılım.
Sevgiyle yapıldı © {{cc}}. + upload_img: + name: Değiştir + loading: yükleniyor... + pic_auth_code: + title: Captcha + placeholder: Yukarıdaki metni yazın + msg: + empty: Captcha boş olamaz. + inactive: + first: >- + Neredeyse tamamlandı! {{mail}} adresine bir aktivasyon e-postası gönderdik. Hesabınızı etkinleştirmek için lütfen e-postadaki talimatları izleyin. + info: "E-posta gelmezse, spam klasörünüzü kontrol edin." + another: >- + {{mail}} adresine başka bir aktivasyon e-postası gönderdik. Gelmesi birkaç dakika sürebilir; spam klasörünüzü kontrol etmeyi unutmayın. + btn_name: Aktivasyon e-postasını yeniden gönder + change_btn_name: E-posta değiştir + msg: + empty: Boş olamaz. + resend_email: + url_label: Aktivasyon e-postasını yeniden göndermek istediğinizden emin misiniz? + url_text: Ayrıca yukarıdaki aktivasyon bağlantısını kullanıcıya verebilirsiniz. + login: + login_to_continue: Devam etmek için giriş yapın + info_sign: Hesabınız yok mu? <1>Kaydolun + info_login: Zaten hesabınız var mı? <1>Giriş yapın + agreements: Kaydolarak <1>gizlilik politikasını ve <3>hizmet şartlarını kabul etmiş olursunuz. + forgot_pass: Parolanızı mı unuttunuz? + name: + label: İsim + msg: + empty: İsim boş olamaz. + range: İsim 2 ile 30 karakter arasında olmalıdır. + character: '"a-z", "A-Z", "0-9", "- . _" karakter setini kullanmalısınız' + email: + label: E-posta + msg: + empty: E-posta boş olamaz. + password: + label: Parola + msg: + empty: Parola boş olamaz. + different: Her iki tarafta girilen parolalar tutarsız + account_forgot: + page_title: Parolanızı mı Unuttunuz + btn_name: Bana kurtarma e-postası gönder + send_success: >- + Eğer bir hesap {{mail}} ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız. + email: + label: E-posta + msg: + empty: E-posta boş olamaz. + change_email: + btn_cancel: İptal + btn_update: E-posta adresini güncelle + send_success: >- + Eğer bir hesap {{mail}} ile eşleşirse, kısa süre içinde parolanızı nasıl sıfırlayacağınıza dair talimatlar içeren bir e-posta almalısınız. + email: + label: Yeni e-posta + msg: + empty: E-posta boş olamaz. + oauth: + connect: '{{auth_name}} ile bağlan' + remove: '{{auth_name}} kaldır' + oauth_bind_email: + subtitle: Hesabınıza bir kurtarma e-postası ekleyin. + btn_update: E-posta adresini güncelle + email: + label: E-posta + msg: + empty: E-posta boş olamaz. + modal_title: E-posta zaten mevcut. + modal_content: Bu e-posta adresi zaten kayıtlı. Mevcut hesaba bağlanmak istediğinizden emin misiniz? + modal_cancel: E-posta değiştir + modal_confirm: Mevcut hesaba bağlan + password_reset: + page_title: Parola Sıfırlama + btn_name: Parolamı sıfırla + reset_success: >- + Parolanızı başarıyla değiştirdiniz; giriş sayfasına yönlendirileceksiniz. + link_invalid: >- + Üzgünüz, bu parola sıfırlama bağlantısı artık geçerli değil. Belki de parolanız zaten sıfırlanmış? + to_login: Giriş sayfasına devam et + password: + label: Parola + msg: + empty: Parola boş olamaz. + length: Uzunluk 8 ile 32 arasında olmalıdır + different: Her iki tarafta girilen parolalar tutarsız + password_confirm: + label: Yeni parolayı onayla + settings: + page_title: Ayarlar + goto_modify: Değiştirmeye git + nav: + profile: Profil + notification: Bildirimler + account: Hesap + interface: Arayüz + profile: + heading: Profil + btn_name: Kaydet + display_name: + label: Görünen ad + msg: Görünen ad boş olamaz. + msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır. + username: + label: Kullanıcı adı + caption: İnsanlar size "@kullaniciadi" şeklinde bahsedebilir. + msg: Kullanıcı adı boş olamaz. + msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır. + character: '"a-z", "0-9", "- . _" karakter setini kullanmalısınız' + avatar: + label: Profil resmi + gravatar: Gravatar + gravatar_text: Resmi şurada değiştirebilirsiniz + custom: Özel + custom_text: Kendi resminizi yükleyebilirsiniz. + default: Sistem + msg: Lütfen bir avatar yükleyin + bio: + label: Hakkımda + website: + label: Website + placeholder: "https://example.com" + msg: Website formatı yanlış + location: + label: Konum + placeholder: "Şehir, Ülke" + notification: + heading: E-posta Bildirimleri + turn_on: Aç + inbox: + label: Gelen kutusu bildirimleri + description: Sorularınıza cevaplar, yorumlar, davetler ve daha fazlası. + all_new_question: + label: Tüm yeni sorular + description: Tüm yeni sorulardan haberdar olun. Haftada en fazla 50 soru. + all_new_question_for_following_tags: + label: Takip edilen etiketler için tüm yeni sorular + description: Takip ettiğiniz etiketlerdeki yeni sorulardan haberdar olun. + account: + heading: Hesap + change_email_btn: E-posta değiştir + change_pass_btn: Parola değiştir + change_email_info: >- + Bu adrese bir e-posta gönderdik. Lütfen onay talimatlarını takip edin. + email: + label: E-posta + new_email: + label: Yeni e-posta + msg: Yeni e-posta boş olamaz. + pass: + label: Mevcut parola + msg: Parola boş olamaz. + password_title: Parola + current_pass: + label: Mevcut parola + msg: + empty: Mevcut parola boş olamaz. + length: Uzunluk 8 ile 32 arasında olmalıdır. + different: Girilen iki parola eşleşmiyor. + new_pass: + label: Yeni parola + pass_confirm: + label: Yeni parolayı onayla + interface: + heading: Arayüz + lang: + label: Arayüz dili + text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir. + my_logins: + title: Girişlerim + label: Bu hesapları kullanarak bu sitede giriş yapın veya kaydolun. + modal_title: Girişi kaldır + modal_content: Bu girişi hesabınızdan kaldırmak istediğinizden emin misiniz? + modal_confirm_btn: Kaldır + remove_success: Başarıyla kaldırıldı + toast: + update: güncelleme başarılı + update_password: Parola başarıyla değiştirildi. + flag_success: Bildirdiğiniz için teşekkürler. + forbidden_operate_self: Kendinizle ilgili işlem yapmak yasaktır + review: Revizyonunuz incelendikten sonra görünecek. + sent_success: Başarıyla gönderildi + related_question: + title: Related + answers: cevap + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: İnsanları Davet Et + desc: Cevap verebileceğini düşündüğünüz kişileri davet edin. + invite: Cevaplamaya davet et + add: Kişi ekle + search: Kişi ara + question_detail: + action: Eylem + Asked: Soruldu + asked: sordu + update: Değiştirildi + edit: düzenledi + commented: yorum yaptı + Views: Görüntülendi + Follow: Takip Et + Following: Takip Ediliyor + follow_tip: Bildirim almak için bu soruyu takip edin + answered: cevapladı + closed_in: Şurada kapatıldı + show_exist: Var olan soruyu göster. + useful: Faydalı + question_useful: Faydalı ve açık + question_un_useful: Belirsiz veya faydalı değil + question_bookmark: Bu soruyu yer işaretlerine ekle + answer_useful: Faydalı + answer_un_useful: Faydalı değil + answers: + title: Cevaplar + score: Puan + newest: En Yeni + oldest: En Eski + btn_accept: Kabul Et + btn_accepted: Kabul Edildi + write_answer: + title: Cevabınız + edit_answer: Mevcut cevabımı düzenle + btn_name: Cevabınızı gönderin + add_another_answer: Başka bir cevap ekle + confirm_title: Cevaplamaya devam et + continue: Devam et + confirm_info: >- +

Başka bir cevap eklemek istediğinizden emin misiniz?

Bunun yerine mevcut cevabınızı iyileştirmek ve geliştirmek için düzenleme bağlantısını kullanabilirsiniz.

+ empty: Cevap boş olamaz. + characters: içerik en az 6 karakter uzunluğunda olmalıdır. + tips: + header_1: Cevabınız için teşekkürler + li1_1: Lütfen soruyu cevapladığınızdan emin olun. Detaylar verin ve araştırmanızı paylaşın. + li1_2: Yaptığınız ifadeleri referanslar veya kişisel deneyimlerle destekleyin. + header_2: Ancak şunlardan kaçının ... + li2_1: Yardım istemek, açıklama istemek veya diğer cevaplara yanıt vermek. + reopen: + confirm_btn: Yeniden Aç + title: Bu gönderiyi yeniden aç + content: Yeniden açmak istediğinizden emin misiniz? + list: + confirm_btn: Listele + title: Bu gönderiyi listele + content: Listelemek istediğinizden emin misiniz? + unlist: + confirm_btn: Listeden Kaldır + title: Bu gönderiyi listeden kaldır + content: Listeden kaldırmak istediğinizden emin misiniz? + pin: + title: Bu gönderiyi sabitle + content: Küresel olarak sabitlemek istediğinizden emin misiniz? Bu gönderi tüm gönderi listelerinin en üstünde görünecektir. + confirm_btn: Sabitle + delete: + title: Bu gönderiyi sil + question: >- + Cevapları olan soruları silmenizi önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır.

Cevaplanmış soruları tekrar tekrar silmek, hesabınızın soru sorma yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz? + answer_accepted: >- +

Kabul edilmiş cevabı silmenizi önermiyoruz çünkü bunu yapmak gelecekteki okuyucuları bu bilgiden mahrum bırakır.

Kabul edilmiş cevapları tekrar tekrar silmek, hesabınızın cevap verme yeteneğinin engellenmesine neden olabilir. Silmek istediğinizden emin misiniz? + other: Silmek istediğinizden emin misiniz? + tip_answer_deleted: Bu cevap silinmiştir + undelete_title: Bu gönderinin silmesini geri al + undelete_desc: Silmeyi geri almak istediğinizden emin misiniz? + btns: + confirm: Onayla + cancel: İptal + edit: Düzenle + save: Kaydet + delete: Sil + undelete: Silmeyi Geri Al + list: Listele + unlist: Listeden Kaldır + unlisted: Listede Değil + login: Giriş Yap + signup: Kayıt Ol + logout: Çıkış Yap + verify: Doğrula + create: Oluştur + approve: Onayla + reject: Reddet + skip: Atla + discard_draft: Taslağı at + pinned: Sabitlendi + all: Tümü + question: Soru + answer: Cevap + comment: Yorum + refresh: Yenile + resend: Yeniden Gönder + deactivate: Devre Dışı Bırak + active: Aktif + suspend: Askıya Al + unsuspend: Askıyı Kaldır + close: Kapat + reopen: Yeniden Aç + ok: Tamam + light: Açık + dark: Koyu + system_setting: Sistem ayarı + default: Varsayılan + reset: Sıfırla + tag: Etiket + post_lowercase: gönderi + filter: Filtrele + ignore: Yoksay + submit: Gönder + normal: Normal + closed: Kapalı + deleted: Silindi + deleted_permanently: Kalıcı olarak silindi + pending: Beklemede + more: Daha Fazla + view: Görüntüle + card: Kart + compact: Kompakt + display_below: Aşağıda göster + always_display: Her zaman göster + or: veya + back_sites: Sitelere geri dön + search: + title: Arama Sonuçları + keywords: Anahtar Kelimeler + options: Seçenekler + follow: Takip Et + following: Takip Ediliyor + counts: "{{count}} Sonuç" + counts_loading: "... Results" + more: Daha Fazla + sort_btns: + relevance: İlgililik + newest: En Yeni + active: Aktif + score: Puan + more: Daha Fazla + tips: + title: Gelişmiş Arama İpuçları + tag: "<1>[etiket] bir etiketle ara" + user: "<1>user:kullanıcıadı yazara göre ara" + answer: "<1>answers:0 cevaplanmamış sorular" + score: "<1>score:3 3+ puana sahip gönderiler" + question: "<1>is:question soruları ara" + is_answer: "<1>is:answer cevapları ara" + empty: Hiçbir şey bulamadık.
Farklı veya daha az spesifik anahtar kelimeler deneyin. + share: + name: Paylaş + copy: Bağlantıyı kopyala + via: Gönderiyi şurada paylaş... + copied: Kopyalandı + facebook: Facebook'ta Paylaş + twitter: X'te Paylaş + cannot_vote_for_self: Kendi gönderinize oy veremezsiniz. + modal_confirm: + title: Hata... + delete_permanently: + title: Kalıcı olarak sil + content: Kalıcı olarak silmek istediğinizden emin misiniz? + account_result: + success: Yeni hesabınız onaylandı; ana sayfaya yönlendirileceksiniz. + link: Ana sayfaya devam et + oops: Hay aksi! + invalid: Kullandığınız bağlantı artık çalışmıyor. + confirm_new_email: E-postanız güncellendi. + confirm_new_email_invalid: >- + Üzgünüz, bu onay bağlantısı artık geçerli değil. Belki e-postanız zaten değiştirildi? + unsubscribe: + page_title: Abonelikten Çık + success_title: Abonelikten Çıkma Başarılı + success_desc: Bu abone listesinden başarıyla çıkarıldınız ve bizden başka e-posta almayacaksınız. + link: Ayarları değiştir + question: + following_tags: Takip Edilen Etiketler + edit: Düzenle + save: Kaydet + follow_tag_tip: Soru listenizi oluşturmak için etiketleri takip edin. + hot_questions: Popüler Sorular + all_questions: Tüm Sorular + x_questions: "{{ count }} Soru" + x_answers: "{{ count }} cevap" + x_posts: "{{ count }} Posts" + questions: Sorular + answers: Cevaplar + newest: En Yeni + active: Aktif + hot: Popüler + frequent: Sık Sorulan + recommend: Önerilenler + score: Puan + unanswered: Cevaplanmamış + modified: değiştirildi + answered: cevaplandı + asked: soruldu + closed: kapandı + follow_a_tag: Bir etiketi takip et + more: Daha Fazla + personal: + overview: Genel Bakış + answers: Cevaplar + answer: cevap + questions: Sorular + question: soru + bookmarks: Yer İşaretleri + reputation: İtibar + comments: Yorumlar + votes: Oylar + badges: Rozetler + newest: En Yeni + score: Puan + edit_profile: Profili düzenle + visited_x_days: "{{ count }} gün ziyaret edildi" + viewed: Görüntülendi + joined: Katıldı + comma: "," + last_login: Görüldü + about_me: Hakkımda + about_me_empty: "// Merhaba, Dünya !" + top_answers: En İyi Cevaplar + top_questions: En İyi Sorular + stats: İstatistikler + list_empty: Gönderi bulunamadı.
Belki farklı bir sekme seçmek istersiniz? + content_empty: Gönderi bulunamadı. + accepted: Kabul Edildi + answered: cevaplandı + asked: sordu + downvoted: negatif oylandı + mod_short: MOD + mod_long: Moderatörler + x_reputation: itibar + x_votes: alınan oy + x_answers: cevap + x_questions: soru + recent_badges: Son Rozetler + install: + title: Kurulum + next: İleri + done: Tamamlandı + config_yaml_error: config.yaml dosyası oluşturulamıyor. + lang: + label: Lütfen bir dil seçin + db_type: + label: Veritabanı motoru + db_username: + label: Kullanıcı adı + placeholder: root + msg: Kullanıcı adı boş olamaz. + db_password: + label: Parola + placeholder: root + msg: Parola boş olamaz. + db_host: + label: Veritabanı sunucusu + placeholder: "db:3306" + msg: Veritabanı sunucusu boş olamaz. + db_name: + label: Veritabanı adı + placeholder: answer + msg: Veritabanı adı boş olamaz. + db_file: + label: Veritabanı dosyası + placeholder: /data/answer.db + msg: Veritabanı dosyası boş olamaz. + ssl_enabled: + label: SSL'i Etkinleştir + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Modu + ssl_root_cert: + placeholder: sslrootcert dosya yolu + msg: sslrootcert dosya yolu boş olamaz + ssl_cert: + placeholder: sslcert dosya yolu + msg: sslcert dosya yolu boş olamaz + ssl_key: + placeholder: sslkey dosya yolu + msg: sslkey dosya yolu boş olamaz + config_yaml: + title: config.yaml Oluştur + label: config.yaml dosyası oluşturuldu. + desc: >- + <1>config.yaml dosyasını <1>/var/wwww/xxx/ dizininde manuel olarak oluşturabilir ve aşağıdaki metni içine yapıştırabilirsiniz. + info: Bunu yaptıktan sonra "İleri" düğmesine tıklayın. + site_information: Site Bilgisi + admin_account: Yönetici Hesabı + site_name: + label: Site adı + msg: Saat dilimi boş olamaz. + msg_max_length: Site adı en fazla 30 karakter uzunluğunda olmalıdır. + site_url: + label: Site URL'si + text: Sitenizin adresi. + msg: + empty: Site URL'si boş olamaz. + incorrect: Site URL'si formatı yanlış. + max_length: Site URL'si en fazla 512 karakter uzunluğunda olmalıdır. + contact_email: + label: İletişim e-postası + text: Bu siteden sorumlu kilit kişinin e-posta adresi. + msg: + empty: İletişim e-postası boş olamaz. + incorrect: İletişim e-postası formatı yanlış. + login_required: + label: Özel + switch: Giriş gerekli + text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir. + admin_name: + label: İsim + msg: İsim boş olamaz. + character: '"a-z", "A-Z", "0-9", "- . _" karakter setini kullanmalısınız' + msg_max_length: İsim 2 ile 30 karakter arasında olmalıdır. + admin_password: + label: Parola + text: >- + Giriş yapmak için bu parolaya ihtiyacınız olacak. Lütfen güvenli bir yerde saklayın. + msg: Parola boş olamaz. + msg_min_length: Parola en az 8 karakter uzunluğunda olmalıdır. + msg_max_length: Parola en fazla 32 karakter uzunluğunda olmalıdır. + admin_confirm_password: + label: "Parolayı Onayla" + text: "Lütfen onaylamak için parolanızı tekrar girin." + msg: "Onay parolası eşleşmiyor." + admin_email: + label: E-posta + text: Giriş yapmak için bu e-postaya ihtiyacınız olacak. + msg: + empty: E-posta boş olamaz. + incorrect: E-posta formatı yanlış. + ready_title: Siteniz hazır + ready_desc: >- + Daha fazla ayarı değiştirmek isterseniz, <1>yönetici bölümünü ziyaret edin; site menüsünde bulabilirsiniz. + good_luck: "İyi eğlenceler ve iyi şanslar!" + warn_title: Uyarı + warn_desc: >- + <1>config.yaml dosyası zaten var. Bu dosyadaki herhangi bir yapılandırma öğesini sıfırlamanız gerekiyorsa, lütfen önce dosyayı silin. + install_now: <1>Şimdi kurulum yapmayı deneyebilirsiniz. + installed: Zaten kurulu + installed_desc: >- + Zaten kurulum yapmış görünüyorsunuz. Yeniden kurmak için lütfen önce eski veritabanı tablolarınızı temizleyin. + db_failed: Veritabanı bağlantısı başarısız + db_failed_desc: >- + Bu, <1>config.yaml dosyanızdaki veritabanı bilgilerinin yanlış olduğu veya veritabanı sunucusuyla bağlantı kurulamadığı anlamına gelir. Bu, sunucunuzun veritabanı sunucusunun çalışmadığı anlamına gelebilir. + counts: + views: görüntülenme + votes: oy + answers: cevap + accepted: Kabul Edildi + page_error: + http_error: HTTP Hatası {{ code }} + desc_403: Bu sayfaya erişim izniniz yok. + desc_404: Maalesef, bu sayfa mevcut değil. + desc_50X: Sunucu bir hatayla karşılaştı ve isteğinizi tamamlayamadı. + back_home: Ana sayfaya dön + page_maintenance: + desc: "Bakım altındayız, yakında geri döneceğiz." + nav_menus: + dashboard: Gösterge Paneli + contents: İçerikler + questions: Sorular + answers: Cevaplar + users: Kullanıcılar + badges: Rozetler + flags: Bildirimler + settings: Ayarlar + general: Genel + interface: Arayüz + smtp: SMTP + branding: Marka + legal: Yasal + write: Yaz + tos: Kullanım Şartları + privacy: Gizlilik + seo: SEO + customize: Özelleştir + themes: Temalar + login: Giriş + privileges: Ayrıcalıklar + plugins: Eklentiler + installed_plugins: Kurulu Eklentiler + apperance: Görünüm + website_welcome: '{{site_name}} sitesine hoş geldiniz' + user_center: + login: Giriş + qrcode_login_tip: Lütfen QR kodu taramak ve giriş yapmak için {{ agentName }} kullanın. + login_failed_email_tip: Giriş başarısız oldu, lütfen tekrar denemeden önce bu uygulamanın e-posta bilgilerinize erişmesine izin verin. + badges: + modal: + title: Tebrikler + content: Yeni bir rozet kazandınız. + close: Kapat + confirm: Rozetleri görüntüle + title: Rozetler + awarded: Kazanıldı + earned_×: '{{ number }} kez kazanıldı' + ×_awarded: "{{ number }} kez verildi" + can_earn_multiple: Bunu birden çok kez kazanabilirsiniz. + earned: Kazanıldı + admin: + admin_header: + title: Yönetici + dashboard: + title: Gösterge Paneli + welcome: Yönetici'ye Hoş Geldiniz! + site_statistics: Site istatistikleri + questions: "Sorular:" + resolved: "Çözülmüş:" + unanswered: "Cevaplanmamış:" + answers: "Cevaplar:" + comments: "Yorumlar:" + votes: "Oylar:" + users: "Kullanıcılar:" + flags: "Bildirimler:" + reviews: "İncelemeler:" + site_health: Site sağlığı + version: "Sürüm:" + https: "HTTPS:" + upload_folder: "Yükleme klasörü:" + run_mode: "Çalışma modu:" + private: Özel + public: Herkese Açık + smtp: "SMTP:" + timezone: "Saat dilimi:" + system_info: Sistem bilgisi + go_version: "Go sürümü:" + database: "Veritabanı:" + database_size: "Veritabanı boyutu:" + storage_used: "Kullanılan depolama:" + uptime: "Çalışma süresi:" + links: Bağlantılar + plugins: Eklentiler + github: GitHub + blog: Blog + contact: İletişim + forum: Forum + documents: Belgeler + feedback: Geribildirim + support: Destek + review: İnceleme + config: Yapılandırma + update_to: Güncelle + latest: En son + check_failed: Kontrol başarısız + "yes": "Evet" + "no": "Hayır" + not_allowed: İzin verilmiyor + allowed: İzin veriliyor + enabled: Etkin + disabled: Devre dışı + writable: Yazılabilir + not_writable: Yazılamaz + flags: + title: Bildirimler + pending: Bekleyen + completed: Tamamlanan + flagged: Bildirilen + flagged_type: Bildirilen {{ type }} + created: Oluşturuldu + action: Eylem + review: İnceleme + user_role_modal: + title: Kullanıcı rolünü şuna değiştir... + btn_cancel: İptal + btn_submit: Gönder + new_password_modal: + title: Yeni parola belirle + form: + fields: + password: + label: Parola + text: Kullanıcının oturumu kapatılacak ve tekrar giriş yapması gerekecek. + msg: Parola 8-32 karakter uzunluğunda olmalıdır. + btn_cancel: İptal + btn_submit: Gönder + edit_profile_modal: + title: Profili düzenle + form: + fields: + display_name: + label: Görünen ad + msg_range: Görünen ad 2-30 karakter uzunluğunda olmalıdır. + username: + label: Kullanıcı adı + msg_range: Kullanıcı adı 2-30 karakter uzunluğunda olmalıdır. + email: + label: E-posta + msg_invalid: Geçersiz E-posta Adresi. + edit_success: Başarıyla düzenlendi + btn_cancel: İptal + btn_submit: Gönder + user_modal: + title: Yeni kullanıcı ekle + form: + fields: + users: + label: Toplu kullanıcı ekle + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: İsim, e-posta, parola bilgilerini virgülle ayırın. Her satırda bir kullanıcı. + msg: "Lütfen kullanıcının e-postasını girin, her satırda bir tane." + display_name: + label: Görünen ad + msg: Görünen ad 2-30 karakter uzunluğunda olmalıdır. + email: + label: E-posta + msg: E-posta geçerli değil. + password: + label: Parola + msg: Parola 8-32 karakter uzunluğunda olmalıdır. + btn_cancel: İptal + btn_submit: Gönder + users: + title: Kullanıcılar + name: İsim + email: E-posta + reputation: İtibar + created_at: Oluşturulma zamanı + delete_at: Silinme zamanı + suspend_at: Askıya Alınma Zamanı + suspend_until: Suspend until + status: Durum + role: Rol + action: Eylem + change: Değiştir + all: Tümü + staff: Ekip + more: Daha Fazla + inactive: Etkin Değil + suspended: Askıya Alınmış + deleted: Silinmiş + normal: Normal + Moderator: Moderatör + Admin: Yönetici + User: Kullanıcı + filter: + placeholder: "İsme göre filtreleme, user:id" + set_new_password: Yeni parola belirle + edit_profile: Profili düzenle + change_status: Durumu değiştir + change_role: Rolü değiştir + show_logs: Kayıtları göster + add_user: Kullanıcı ekle + deactivate_user: + title: Kullanıcıyı devre dışı bırak + content: Etkin olmayan bir kullanıcının e-postasını yeniden doğrulaması gerekir. + delete_user: + title: Bu kullanıcıyı sil + content: Bu kullanıcıyı silmek istediğinizden emin misiniz? Bu kalıcıdır! + remove: İçeriklerini kaldır + label: Tüm soruları, cevapları, yorumları vb. kaldır. + text: Sadece kullanıcının hesabını silmek istiyorsanız bunu işaretlemeyin. + suspend_user: + title: Bu kullanıcıyı askıya al + content: Askıya alınmış bir kullanıcı giriş yapamaz. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Sorular + unlisted: Listelenmemiş + post: Gönderi + votes: Oylar + answers: Cevaplar + created: Oluşturuldu + status: Durum + action: Eylem + change: Değiştir + pending: Beklemede + filter: + placeholder: "Başlığa göre filtreleme, question:id" + answers: + page_title: Cevaplar + post: Gönderi + votes: Oylar + created: Oluşturuldu + status: Durum + action: Eylem + change: Değiştir + filter: + placeholder: "Başlığa göre filtreleme, answer:id" + general: + page_title: Genel + name: + label: Site adı + msg: Site adı boş olamaz. + text: "Başlık etiketinde kullanılan bu sitenin adı." + site_url: + label: Site URL'si + msg: Site url'si boş olamaz. + validate: Lütfen geçerli bir URL girin. + text: Sitenizin adresi. + short_desc: + label: Kısa site açıklaması + msg: Kısa site açıklaması boş olamaz. + text: "Ana sayfadaki başlık etiketinde kullanılan kısa açıklama." + desc: + label: Site açıklaması + msg: Site açıklaması boş olamaz. + text: "Bu siteyi bir cümleyle açıklayın, meta açıklama etiketinde kullanılır." + contact_email: + label: İletişim e-postası + msg: İletişim e-postası boş olamaz. + validate: İletişim e-postası geçerli değil. + text: Bu siteden sorumlu kilit kişinin e-posta adresi. + check_update: + label: Yazılım güncellemeleri + text: Güncellemeleri otomatik olarak kontrol et + interface: + page_title: Arayüz + language: + label: Arayüz dili + msg: Arayüz dili boş olamaz. + text: Kullanıcı arayüzü dili. Sayfa yenilendiğinde değişecektir. + time_zone: + label: Saat dilimi + msg: Saat dilimi boş olamaz. + text: Sizinle aynı saat dilimindeki bir şehri seçin. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Gönderen e-posta + msg: Gönderen e-posta boş olamaz. + text: E-postaların gönderildiği e-posta adresi. + from_name: + label: Gönderen adı + msg: Gönderen adı boş olamaz. + text: E-postaların gönderildiği isim. + smtp_host: + label: SMTP sunucusu + msg: SMTP sunucusu boş olamaz. + text: Mail sunucunuz. + encryption: + label: Şifreleme + msg: Şifreleme boş olamaz. + text: Çoğu sunucu için SSL önerilen seçenektir. + ssl: SSL + tls: TLS + none: Yok + smtp_port: + label: SMTP portu + msg: SMTP portu 1 ~ 65535 arasında bir sayı olmalıdır. + text: Mail sunucunuzun portu. + smtp_username: + label: SMTP kullanıcı adı + msg: SMTP kullanıcı adı boş olamaz. + smtp_password: + label: SMTP parolası + msg: SMTP parolası boş olamaz. + test_email_recipient: + label: Test e-posta alıcıları + text: Test gönderimlerini alacak e-posta adresini girin. + msg: Test e-posta alıcıları geçersiz + smtp_authentication: + label: Kimlik doğrulamayı etkinleştir + title: SMTP kimlik doğrulaması + msg: SMTP kimlik doğrulaması boş olamaz. + "yes": "Evet" + "no": "Hayır" + branding: + page_title: Marka + logo: + label: Logo + msg: Logo boş olamaz. + text: Sitenizin sol üst köşesindeki logo resmi. 56 yüksekliğinde ve 3:1'den büyük en-boy oranlı geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, site başlık metni gösterilecektir. + mobile_logo: + label: Mobil logo + text: Sitenizin mobil versiyonunda kullanılan logo. 56 yüksekliğinde geniş dikdörtgen bir resim kullanın. Boş bırakılırsa, "logo" ayarındaki resim kullanılacaktır. + square_icon: + label: Kare simge + msg: Kare simge boş olamaz. + text: Meta veri simgeleri için temel olarak kullanılan resim. İdeal olarak 512x512'den büyük olmalıdır. + favicon: + label: Favicon + text: Siteniz için bir favicon. CDN üzerinde düzgün çalışması için png olmalıdır. 32x32 boyutuna yeniden boyutlandırılacaktır. Boş bırakılırsa, "kare simge" kullanılacaktır. + legal: + page_title: Yasal + terms_of_service: + label: Kullanım şartları + text: "Buraya kullanım şartları içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin." + privacy_policy: + label: Gizlilik politikası + text: "Buraya gizlilik politikası içeriği ekleyebilirsiniz. Başka bir yerde barındırılan bir belgeniz varsa, tam URL'yi buraya girin." + external_content_display: + label: Harici içerik + text: "İçerik, harici web sitelerinden gömülen resimler, videolar ve medyayı içerir." + always_display: Her zaman harici içeriği göster + ask_before_display: Harici içeriği göstermeden önce sor + write: + page_title: Yazma + restrict_answer: + title: Cevap yazma + label: Her kullanıcı aynı soru için sadece bir cevap yazabilir + text: "Kullanıcıların aynı soruya birden fazla cevap yazmasına izin vermek için kapatın, bu cevapların odaktan uzaklaşmasına neden olabilir." + recommend_tags: + label: Önerilen etiketler + text: "Önerilen etiketler varsayılan olarak açılır listede gösterilecektir." + msg: + contain_reserved: "önerilen etiketler ayrılmış etiketleri içeremez" + required_tag: + title: Gerekli etiketleri ayarla + label: Önerilen etiketleri gerekli etiketler olarak ayarla + text: "Her yeni soru en az bir önerilen etikete sahip olmalıdır." + reserved_tags: + label: Ayrılmış etiketler + text: "Ayrılmış etiketler sadece moderatör tarafından kullanılabilir." + image_size: + label: Maksimum resim boyutu (MB) + text: "Maksimum resim yükleme boyutu." + attachment_size: + label: Maksimum ek dosya boyutu (MB) + text: "Maksimum ek dosya yükleme boyutu." + image_megapixels: + label: Maksimum resim megapikseli + text: "Bir resim için izin verilen maksimum megapiksel sayısı." + image_extensions: + label: İzin verilen resim uzantıları + text: "Resim gösterimi için izin verilen dosya uzantılarının listesi, virgülle ayırın." + attachment_extensions: + label: İzin verilen ek dosya uzantıları + text: "Yükleme için izin verilen dosya uzantılarının listesi, virgülle ayırın. UYARI: Yüklemelere izin vermek güvenlik sorunlarına neden olabilir." + seo: + page_title: SEO + permalink: + label: Kalıcı bağlantı + text: Özel URL yapıları, bağlantılarınızın kullanılabilirliğini ve ileriye dönük uyumluluğunu iyileştirebilir. + robots: + label: robots.txt + text: Bu, ilgili tüm site ayarlarını kalıcı olarak geçersiz kılacaktır. + themes: + page_title: Temalar + themes: + label: Temalar + text: Mevcut bir tema seçin. + color_scheme: + label: Renk şeması + navbar_style: + label: Gezinme çubuğu arka plan stili + primary_color: + label: Ana renk + text: Temalarınızda kullanılan renkleri değiştirin + css_and_html: + page_title: CSS ve HTML + custom_css: + label: Özel CSS + text: > + Bu <link> olarak eklenecektir + head: + label: Head + text: > + Bu </head> öncesine eklenecektir + header: + label: Header + text: > + Bu <body> sonrasına eklenecektir + footer: + label: Footer + text: Bu </body> öncesine eklenecektir + sidebar: + label: Kenar çubuğu + text: Bu kenar çubuğuna eklenecektir. + login: + page_title: Giriş + membership: + title: Üyelik + label: Yeni kayıtlara izin ver + text: Herhangi birinin yeni hesap oluşturmasını engellemek için kapatın. + email_registration: + title: E-posta kaydı + label: E-posta kaydına izin ver + text: Herhangi birinin e-posta yoluyla yeni hesap oluşturmasını engellemek için kapatın. + allowed_email_domains: + title: İzin verilen e-posta alan adları + text: Kullanıcıların hesap kaydı yapması gereken e-posta alan adları. Her satırda bir alan adı. Boş olduğunda dikkate alınmaz. + private: + title: Özel + label: Giriş gerekli + text: Bu topluluğa sadece giriş yapmış kullanıcılar erişebilir. + password_login: + title: Parola ile giriş + label: E-posta ve parola ile girişe izin ver + text: "UYARI: Kapatırsanız, daha önce başka bir giriş yöntemi yapılandırmadıysanız giriş yapamayabilirsiniz." + installed_plugins: + title: Kurulu Eklentiler + plugin_link: Eklentiler işlevselliği genişletir ve artırır. Eklentileri <1>Eklenti Deposu'nda bulabilirsiniz. + filter: + all: Tümü + active: Aktif + inactive: Pasif + outdated: Güncel değil + plugins: + label: Eklentiler + text: Mevcut bir eklenti seçin. + name: İsim + version: Sürüm + status: Durum + action: Eylem + deactivate: Devre dışı bırak + activate: Etkinleştir + settings: Ayarlar + settings_users: + title: Kullanıcılar + avatar: + label: Varsayılan avatar + text: Kendi özel avatarı olmayan kullanıcılar için. + gravatar_base_url: + label: Gravatar temel URL'si + text: Gravatar sağlayıcısının API temel URL'si. Boş olduğunda dikkate alınmaz. + profile_editable: + title: Profil düzenlenebilir + allow_update_display_name: + label: Kullanıcıların görünen adlarını değiştirmelerine izin ver + allow_update_username: + label: Kullanıcıların kullanıcı adlarını değiştirmelerine izin ver + allow_update_avatar: + label: Kullanıcıların profil resimlerini değiştirmelerine izin ver + allow_update_bio: + label: Kullanıcıların hakkında bilgilerini değiştirmelerine izin ver + allow_update_website: + label: Kullanıcıların web sitelerini değiştirmelerine izin ver + allow_update_location: + label: Kullanıcıların konumlarını değiştirmelerine izin ver + privilege: + title: Ayrıcalıklar + level: + label: Gereken itibar seviyesi + text: Ayrıcalıklar için gereken itibarı seçin + msg: + should_be_number: giriş bir sayı olmalıdır + number_larger_1: sayı 1'e eşit veya daha büyük olmalıdır + badges: + action: Eylem + active: Aktif + activate: Etkinleştir + all: Tümü + awards: Ödüller + deactivate: Devre dışı bırak + filter: + placeholder: İsme göre filtreleme, badge:id + group: Grup + inactive: Pasif + name: İsim + show_logs: Kayıtları göster + status: Durum + title: Rozetler + form: + optional: (isteğe bağlı) + empty: boş olamaz + invalid: geçersiz + btn_submit: Kaydet + not_found_props: "Gerekli {{ key }} özelliği bulunamadı." + select: Seç + page_review: + review: İnceleme + proposed: önerilen + question_edit: Soru düzenleme + answer_edit: Cevap düzenleme + tag_edit: Etiket düzenleme + edit_summary: Düzenleme özeti + edit_question: Soruyu düzenle + edit_answer: Cevabı düzenle + edit_tag: Etiketi düzenle + empty: İncelenecek görev kalmadı. + approve_revision_tip: Bu revizyonu onaylıyor musunuz? + approve_flag_tip: Bu bildirimi onaylıyor musunuz? + approve_post_tip: Bu gönderiyi onaylıyor musunuz? + approve_user_tip: Bu kullanıcıyı onaylıyor musunuz? + suggest_edits: Önerilen düzenlemeler + flag_post: Gönderiyi bildir + flag_user: Kullanıcıyı bildir + queued_post: Sıradaki gönderi + queued_user: Sıradaki kullanıcı + filter_label: Tür + reputation: itibar + flag_post_type: Bu gönderiyi {{ type }} olarak bildirdi. + flag_user_type: Bu kullanıcıyı {{ type }} olarak bildirdi. + edit_post: Gönderiyi düzenle + list_post: Gönderiyi listele + unlist_post: Gönderiyi listeden kaldır + timeline: + undeleted: silme geri alındı + deleted: silindi + downvote: negatif oy + upvote: pozitif oy + accept: kabul et + cancelled: iptal edildi + commented: yorum yapıldı + rollback: geri alındı + edited: düzenlendi + answered: cevaplandı + asked: soruldu + closed: kapatıldı + reopened: yeniden açıldı + created: oluşturuldu + pin: sabitlendi + unpin: sabitlenme kaldırıldı + show: listelendi + hide: listelenmedi + title: "Geçmiş:" + tag_title: "Zaman çizelgesi:" + show_votes: "Oyları göster" + n_or_a: Yok + title_for_question: "Zaman çizelgesi:" + title_for_answer: "{{ author }} tarafından {{ title }} sorusuna verilen cevabın zaman çizelgesi" + title_for_tag: "Etiket için zaman çizelgesi" + datetime: Tarih/Saat + type: Tür + by: Yapan + comment: Yorum + no_data: "Hiçbir şey bulamadık." + users: + title: Kullanıcılar + users_with_the_most_reputation: Bu hafta en yüksek itibar puanına sahip kullanıcılar + users_with_the_most_vote: Bu hafta en çok oy veren kullanıcılar + staffs: Topluluk ekibimiz + reputation: itibar + votes: oy + prompt: + leave_page: Sayfadan ayrılmak istediğinizden emin misiniz? + changes_not_save: Değişiklikleriniz kaydedilmeyebilir. + draft: + discard_confirm: Taslağınızı atmak istediğinizden emin misiniz? + messages: + post_deleted: Bu gönderi silindi. + post_cancel_deleted: Bu gönderinin silme işlemi geri alındı. + post_pin: Bu gönderi sabitlendi. + post_unpin: Bu gönderinin sabitlenmesi kaldırıldı. + post_hide_list: Bu gönderi listede gizlendi. + post_show_list: Bu gönderi listede gösterildi. + post_reopen: Bu gönderi yeniden açıldı. + post_list: Bu gönderi listelendi. + post_unlist: Bu gönderi listeden kaldırıldı. + post_pending: Gönderiniz inceleme bekliyor. Bu bir önizlemedir, onaylandıktan sonra görünür olacaktır. + post_closed: Bu gönderi kapatıldı. + answer_deleted: Bu cevap silindi. + answer_cancel_deleted: Bu cevabın silme işlemi geri alındı. + change_user_role: Bu kullanıcının rolü değiştirildi. + user_inactive: Bu kullanıcı zaten etkin değil. + user_normal: Bu kullanıcı zaten normal durumda. + user_suspended: Bu kullanıcı askıya alındı. + user_deleted: Bu kullanıcı silindi. + badge_activated: Bu rozet etkinleştirildi. + badge_inactivated: Bu rozet devre dışı bırakıldı. + users_deleted: Bu kullanıcılar silindi. + posts_deleted: Bu sorular silindi. + answers_deleted: Bu cevaplar silindi. + copy: Panoya kopyala + copied: Kopyalandı + external_content_warning: Harici resimler/medya gösterilmiyor. + + diff --git a/i18n/uk_UA.yaml b/i18n/uk_UA.yaml new file mode 100644 index 000000000..42603595e --- /dev/null +++ b/i18n/uk_UA.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Успішно. + unknown: + other: Невідома помилка. + request_format_error: + other: Неприпустимий формат запиту. + unauthorized_error: + other: Не авторизовано. + database_error: + other: Помилка сервера даних. + forbidden_error: + other: Заборонено. + duplicate_request_error: + other: Повторний запит. + action: + report: + other: Відмітити + edit: + other: Редагувати + delete: + other: Видалити + close: + other: Закрити + reopen: + other: Відкрити знову + forbidden_error: + other: Заборонено. + pin: + other: Закріпити + hide: + other: Вилучити зі списку + unpin: + other: Відкріпити + show: + other: Список + invite_someone_to_answer: + other: Редагувати + undelete: + other: Скасувати видалення + merge: + other: Merge + role: + name: + user: + other: Користувач + admin: + other: Адмін + moderator: + other: Модератор + description: + user: + other: За замовчуванням без спеціального доступу. + admin: + other: Має повний доступ до сайту. + moderator: + other: Має доступ до всіх дописів, окрім налаштувань адміністратора. + privilege: + level_1: + description: + other: Рівень 1 (для приватної команди, групи потрібна менша репутація) + level_2: + description: + other: Рівень 2 (низька репутація, необхідна для стартап-спільноти) + level_3: + description: + other: Рівень 3 (висока репутація, необхідна для зрілої спільноти) + level_custom: + description: + other: Користувацький рівень + rank_question_add_label: + other: Задати питання + rank_answer_add_label: + other: Написати відповідь + rank_comment_add_label: + other: Написати коментар + rank_report_add_label: + other: Відмітити + rank_comment_vote_up_label: + other: Проголосувати за коментар + rank_link_url_limit_label: + other: Публікуйте більш ніж 2 посилання одночасно + rank_question_vote_up_label: + other: Проголосувати за питання + rank_answer_vote_up_label: + other: Проголосувати за відповідь + rank_question_vote_down_label: + other: Проголосувати проти питання + rank_answer_vote_down_label: + other: Проголосувати проти відповіді + rank_invite_someone_to_answer_label: + other: Запросити когось відповісти + rank_tag_add_label: + other: Створити новий теґ + rank_tag_edit_label: + other: Редагувати опис теґу (необхідно розглянути) + rank_question_edit_label: + other: Редагувати чуже питання (необхідно розглянути) + rank_answer_edit_label: + other: Редагувати чужу відповідь (необхідно розглянути) + rank_question_edit_without_review_label: + other: Редагувати чуже питання без розгляду + rank_answer_edit_without_review_label: + other: Редагувати чужу відповідь без розгляду + rank_question_audit_label: + other: Переглянути редагування питання + rank_answer_audit_label: + other: Переглянути редагування відповіді + rank_tag_audit_label: + other: Переглянути редагування теґу + rank_tag_edit_without_review_label: + other: Редагувати опис теґу без розгляду + rank_tag_synonym_label: + other: Керування синонімами тегів + email: + other: Електронна пошта + e_mail: + other: Електронна пошта + password: + other: Пароль + pass: + other: Пароль + old_pass: + other: Current password + original_text: + other: Цей допис + email_or_password_wrong_error: + other: Електронна пошта та пароль не збігаються. + error: + common: + invalid_url: + other: Невірна URL. + status_invalid: + other: Неприпустимий статус. + password: + space_invalid: + other: Пароль не може містити пробіли. + admin: + cannot_update_their_password: + other: Ви не можете змінити свій пароль. + cannot_edit_their_profile: + other: Ви не можете змінити свій профіль. + cannot_modify_self_status: + other: Ви не можете змінити свій статус. + email_or_password_wrong: + other: Електронна пошта та пароль не збігаються. + answer: + not_found: + other: Відповідь не знайдено. + cannot_deleted: + other: Немає дозволу на видалення. + cannot_update: + other: Немає дозволу на оновлення. + question_closed_cannot_add: + other: Питання закриті й не можуть бути додані. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Коментарі не можна редагувати. + not_found: + other: Коментар не знайдено. + cannot_edit_after_deadline: + other: Час коментаря був занадто довгим, щоб його можна було змінити. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Такий E-mail вже існує. + need_to_be_verified: + other: Електронна пошта повинна бути підтверджена. + verify_url_expired: + other: Термін дії підтвердженої URL-адреси закінчився, будь ласка, надішліть листа повторно. + illegal_email_domain_error: + other: З цього поштового домену заборонено надсилати електронну пошту. Будь ласка, використовуйте інший. + lang: + not_found: + other: Мовний файл не знайдено. + object: + captcha_verification_failed: + other: Неправильно введено капчу. + disallow_follow: + other: Вам не дозволено підписатися. + disallow_vote: + other: Вам не дозволено голосувати. + disallow_vote_your_self: + other: Ви не можете проголосувати за власну публікацію. + not_found: + other: Обʼєкт не знайдено. + verification_failed: + other: Не вдалося виконати перевірку. + email_or_password_incorrect: + other: Електронна пошта та пароль не збігаються. + old_password_verification_failed: + other: Не вдалося перевірити старий пароль + new_password_same_as_previous_setting: + other: Новий пароль збігається з попереднім. + already_deleted: + other: Публікацію видалено. + meta: + object_not_found: + other: Мета-об'єкт не знайдено + question: + already_deleted: + other: Публікацію видалено. + under_review: + other: Ваше повідомлення очікує на розгляд. Його буде видно після того, як воно буде схвалено. + not_found: + other: Питання не знайдено. + cannot_deleted: + other: Немає дозволу на видалення. + cannot_close: + other: Немає дозволу на закриття. + cannot_update: + other: Немає дозволу на оновлення. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Ранг репутації не відповідає умові. + vote_fail_to_meet_the_condition: + other: Дякуємо за відгук. Щоб проголосувати, вам потрібна репутація не нижче {{.Rank}}. + no_enough_rank_to_operate: + other: Щоб це зробити, вам потрібна репутація не менше {{.Rank}}. + report: + handle_failed: + other: Не вдалося обробити звіт. + not_found: + other: Звіт не знайдено. + tag: + already_exist: + other: Теґ уже існує. + not_found: + other: Теґ не знайдено. + recommend_tag_not_found: + other: Рекомендований теґ не існує. + recommend_tag_enter: + other: Будь ласка, введіть принаймні один необхідний тег. + not_contain_synonym_tags: + other: Не повинно містити теґи синонімів. + cannot_update: + other: Немає дозволу на оновлення. + is_used_cannot_delete: + other: Ви не можете видалити теґ, який використовується. + cannot_set_synonym_as_itself: + other: Ви не можете встановити синонім поточного тегу як сам тег. + smtp: + config_from_name_cannot_be_email: + other: Ім’я відправника не може бути електронною адресою. + theme: + not_found: + other: Тему не знайдено. + revision: + review_underway: + other: Наразі неможливо редагувати, є версія в черзі перегляду. + no_permission: + other: Немає дозволу на перегляд. + user: + external_login_missing_user_id: + other: Платформа сторонніх розробників не надає унікальний ідентифікатор користувача, тому ви не можете увійти, будь ласка, зв’яжіться з адміністратором вебсайту. + external_login_unbinding_forbidden: + other: Будь ласка, встановіть пароль для входу до свого облікового запису, перш ніж видалити ім'я користувача. + email_or_password_wrong: + other: + other: Електронна пошта та пароль не збігаються. + not_found: + other: Користувач не знайдений. + suspended: + other: Користувач був призупинений. + username_invalid: + other: Ім'я користувача недійсне. + username_duplicate: + other: Це ім'я користувача вже використовується. + set_avatar: + other: Не вдалося встановити аватар. + cannot_update_your_role: + other: Ви не можете змінити вашу роль. + not_allowed_registration: + other: На цей час сайт не відкритий для реєстрації. + not_allowed_login_via_password: + other: Наразі на сайті заборонено вхід за допомогою пароля. + access_denied: + other: Доступ заборонено + page_access_denied: + other: Ви не маєте доступу до цієї сторінки. + add_bulk_users_format_error: + other: "Помилка формату {{.Field}} біля '{{.Content}}' у рядку {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Кількість користувачів, яких ви додаєте одночасно, має бути в діапазоні 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Не вдалося прочитати конфігурацію + database: + connection_failed: + other: Не вдалося встановити з'єднання з базою даних + create_table_failed: + other: Не вдалося створити таблицю + install: + create_config_failed: + other: Не вдалося створити config.yaml файл. + upload: + unsupported_file_format: + other: Непідтримуваний формат файлу. + site_info: + config_not_found: + other: Конфігурацію сайту не знайдено. + badge: + object_not_found: + other: Об'єкт значка не знайдено + reason: + spam: + name: + other: спам + desc: + other: Це повідомлення є рекламою або вандалізмом. Воно не є корисним або не має відношення до поточної теми. + rude_or_abusive: + name: + other: грубо чи образливо + desc: + other: "Розумна людина вважатиме такий зміст неприйнятним для ввічливого спілкування." + a_duplicate: + name: + other: дублікат + desc: + other: Це питання ставилося раніше, і на нього вже є відповідь. + placeholder: + other: Введіть наявне посилання на питання + not_a_answer: + name: + other: не відповідь + desc: + other: "Це повідомлення було опубліковане як відповідь, але воно не є спробою відповісти на запитання. Можливо, його слід відредагувати, прокоментувати, поставити інше запитання або взагалі видалити." + no_longer_needed: + name: + other: більше не потрібно + desc: + other: Цей коментар є застарілим, розмовним або не стосується цієї публікації. + something: + name: + other: інше + desc: + other: Ця публікація вимагає уваги персоналу з іншої причини, що не вказана вище. + placeholder: + other: Дайте нам знати, що саме вас турбує + community_specific: + name: + other: причина для спільноти + desc: + other: Це запитання не відповідає правилам спільноти. + not_clarity: + name: + other: потребує деталей або ясності + desc: + other: Наразі це запитання містить кілька запитань в одному. Воно має бути зосереджене лише на одній проблемі. + looks_ok: + name: + other: виглядає добре + desc: + other: Цей допис хороший, як є, і не є низької якості. + needs_edit: + name: + other: потребує редагування, і я це зробив + desc: + other: Поліпшіть та виправте проблеми з цією публікацією самостійно. + needs_close: + name: + other: потрібно закрити + desc: + other: Закрите питання не може відповісти, але все ще може редагувати, голосувати і коментувати. + needs_delete: + name: + other: потрібно видалити + desc: + other: Цей допис буде видалено. + question: + close: + duplicate: + name: + other: спам + desc: + other: Це питання ставилося раніше, і на нього вже є відповідь. + guideline: + name: + other: причина для спільноти + desc: + other: Це запитання не відповідає правилам спільноти. + multiple: + name: + other: потребує деталей або ясності + desc: + other: Наразі це питання включає кілька запитань в одному. Воно має зосереджуватися лише на одній проблемі. + other: + name: + other: інше + desc: + other: Для цього допису потрібна інша причина, не зазначена вище. + operation_type: + asked: + other: запитав + answered: + other: відповів + modified: + other: змінено + deleted_title: + other: Видалене питання + questions_title: + other: Питання + tag: + tags_title: + other: Теґи + no_description: + other: Тег не має опису. + notification: + action: + update_question: + other: оновлене питання + answer_the_question: + other: питання з відповіддю + update_answer: + other: оновлена відповідь + accept_answer: + other: прийнята відповідь + comment_question: + other: прокоментоване питання + comment_answer: + other: прокоментована відповідь + reply_to_you: + other: відповів(-ла) вам + mention_you: + other: згадав(-ла) вас + your_question_is_closed: + other: Ваше запитання закрито + your_question_was_deleted: + other: Ваше запитання видалено + your_answer_was_deleted: + other: Вашу відповідь видалено + your_comment_was_deleted: + other: Ваш коментар видалено + up_voted_question: + other: питання, за яке найбільше проголосували + down_voted_question: + other: питання, за яке проголосували менше + up_voted_answer: + other: відповідь, за яку проголосували найбільше + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: коментар, за який проголосували + invited_you_to_answer: + other: запросив(-ла) вас відповісти + earned_badge: + other: Ви заробили бейдж "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Підтвердіть нову адресу електронної пошти" + body: + other: "Підтвердьте свою нову адресу електронної пошти для {{.SiteName}} натиснувши на наступне посилання:
\n{{.ChangeEmailUrl}}

\n\nЯкщо ви не запитували цю зміну, будь ласка, ігноруйте цей лист.

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} відповів(-ла) на ваше запитання" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} запросив(-ла) вас відповісти" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Думаю, ви можете знати відповідь.

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} прокоментували ваш допис" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nПереглянути на {{.SiteName}}

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена.

\n\nВідписатися" + new_question: + title: + other: "[{{.SiteName}}] Нове питання: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Скидання пароля" + body: + other: "Хтось попросив скинути ваш пароль на {{.SiteName}}.

\n\nЯкщо це не ви, можете сміливо ігнорувати цей лист.

\n\nПерейдіть за наступним посиланням, щоб вибрати новий пароль:
\n{{.PassResetUrl}}\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." + register: + title: + other: "[{{.SiteName}}] Підтвердьте свій новий обліковий запис" + body: + other: "Ласкаво просимо до {{.SiteName}}!

\n\nПерейдіть за наступним посиланням, щоб підтвердити та активувати свій новий обліковий запис:
\n{{.RegisterUrl}}

\n\nЯкщо наведене вище посилання не відкривається, спробуйте скопіювати і вставити його в адресний рядок вашого веб-браузера.\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." + test: + title: + other: "[{{.SiteName}}] Тестовий електронний лист" + body: + other: "Це тестовий електронний лист.\n

\n\n--
\nПримітка: Це автоматичний системний електронний лист, будь ласка, не відповідайте на це повідомлення, оскільки ваша відповідь не буде побачена." + action_activity_type: + upvote: + other: підтримати + upvoted: + other: підтримано + downvote: + other: голос "проти" + downvoted: + other: проголосував проти + accept: + other: прийняти + accepted: + other: прийнято + edit: + other: редагувати + review: + queued_post: + other: Допис у черзі + flagged_post: + other: Відмічений пост + suggested_post_edit: + other: Запропоновані зміни + reaction: + tooltip: + other: "{{ .Names }} і {{ .Count }} більше..." + badge: + default_badges: + autobiographer: + name: + other: Автобіограф + desc: + other: Заповнена інформація про профіль. + certified: + name: + other: Підтверджений + desc: + other: Завершено наш новий посібник користувача. + editor: + name: + other: Редактор + desc: + other: Перше редагування посту. + first_flag: + name: + other: Перший прапор + desc: + other: Спочатку позначено допис. + first_upvote: + name: + other: Перший голос за + desc: + other: Першим голосував за допис. + first_link: + name: + other: Перше посилання + desc: + other: First added a link to another post. + first_reaction: + name: + other: Перша реакція + desc: + other: Першим відреагував на допис. + first_share: + name: + other: Перше поширення + desc: + other: Перший поділився публікацією. + scholar: + name: + other: Вчений + desc: + other: Поставив питання і прийняв відповідь. + commentator: + name: + other: Коментатор + desc: + other: Залиште 5 коментарів. + new_user_of_the_month: + name: + other: Новий користувач місяця + desc: + other: Видатні внески за їх перший місяць. + read_guidelines: + name: + other: Прочитайте Інструкцію + desc: + other: Прочитайте [рекомендації для спільноти]. + reader: + name: + other: Читач + desc: + other: Прочитайте кожну відповідь у темі з більш ніж 10 відповідями. + welcome: + name: + other: Ласкаво просимо + desc: + other: Отримав голос. + nice_share: + name: + other: Гарне поширення + desc: + other: Поділилися постом з 25 унікальними відвідувачами. + good_share: + name: + other: Хороше поширення + desc: + other: Поділилися постом з 300 унікальними відвідувачами. + great_share: + name: + other: Відмінне поширення + desc: + other: Поділилися постом з 1000 унікальними відвідувачами. + out_of_love: + name: + other: З любові + desc: + other: Використав 50 голосів «за» за день. + higher_love: + name: + other: Вище кохання + desc: + other: Використав 50 голосів «за» за день 5 разів. + crazy_in_love: + name: + other: Божевільний в любові + desc: + other: Використав 50 голосів «за» за день 20 разів. + promoter: + name: + other: Промоутер + desc: + other: Запросив користувача. + campaigner: + name: + other: Агітатор + desc: + other: Запрошено 3 основних користувачів. + champion: + name: + other: Чемпіон + desc: + other: Запросив 5 учасників. + thank_you: + name: + other: Дякую + desc: + other: Має 20 дописів, за які проголосували, і віддав 10 голосів «за». + gives_back: + name: + other: Дає назад + desc: + other: Має 100 дописів, за які проголосували, і віддав 100 голосів «за». + empathetic: + name: + other: Емпатичний + desc: + other: Має 500 дописів, за які проголосували, і віддав 1000 голосів «за». + enthusiast: + name: + other: Ентузіаст + desc: + other: Відвідано 10 днів поспіль. + aficionado: + name: + other: Шанувальник + desc: + other: Відвідано 100 днів поспіль. + devotee: + name: + other: Відданий + desc: + other: Відвідано 365 днів поспіль. + anniversary: + name: + other: Річниця + desc: + other: Активний учасник на рік, опублікував принаймні один раз. + appreciated: + name: + other: Оцінений + desc: + other: Отримано 1 голос за 20 дописів. + respected: + name: + other: Шанований + desc: + other: Отримано 2 голоси за 100 дописів. + admired: + name: + other: Захоплений + desc: + other: Отримано 5 голосів за 300 дописів. + solved: + name: + other: Вирішено + desc: + other: Нехай відповідь буде прийнята. + guidance_counsellor: + name: + other: Радник супроводу + desc: + other: Прийміть 10 відповідей. + know_it_all: + name: + other: Усезнайко + desc: + other: Було прийнято 50 відповідей. + solution_institution: + name: + other: Інституція рішення + desc: + other: Було прийнято 150 відповідей. + nice_answer: + name: + other: Чудова відповідь + desc: + other: Оцінка відповіді на 10 або більше. + good_answer: + name: + other: Гарна відповідь + desc: + other: Оцінка відповіді на 25 або більше. + great_answer: + name: + other: Чудова відповідь + desc: + other: Оцінка відповіді на 50 або більше. + nice_question: + name: + other: Гарне питання + desc: + other: Оцінка питання на 10 або більше. + good_question: + name: + other: Хороше питання + desc: + other: Оцінка питання на 25 або більше. + great_question: + name: + other: Відмінне питання + desc: + other: Оцінка питання на 50 або більше. + popular_question: + name: + other: Популярне питання + desc: + other: Питання з 500 переглядами. + notable_question: + name: + other: Помітне питання + desc: + other: Питання з 1000 переглядами. + famous_question: + name: + other: Знамените питання + desc: + other: Питання з 5000 переглядами. + popular_link: + name: + other: Популярне посилання + desc: + other: Опубліковано зовнішнє посилання з 50 натисканнями. + hot_link: + name: + other: Гаряче посилання + desc: + other: Опубліковано зовнішнє посилання з 300 натисканнями. + famous_link: + name: + other: Знамените Посилання + desc: + other: Опубліковано зовнішнє посилання зі 100 натисканнями. + default_badge_groups: + getting_started: + name: + other: Початок роботи + community: + name: + other: Спільнота + posting: + name: + other: Публікація +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Як відформатувати + desc: >- + + pagination: + prev: Назад + next: Далі + page_title: + question: Запитання + questions: Запитання + tag: Теґ + tags: Теґи + tag_wiki: тег вікі + create_tag: Створити теґ + edit_tag: Редагувати теґ + ask_a_question: Create Question + edit_question: Редагувати запитання + edit_answer: Редагувати відповідь + search: Пошук + posts_containing: Публікації, що містять + settings: Налаштування + notifications: Сповіщення + login: Увійти + sign_up: Зареєструватися + account_recovery: Відновлення облікового запису + account_activation: Активація облікового запису + confirm_email: Підтвердити електронну адресу + account_suspended: Обліковий запис призупинено + admin: Адмін + change_email: Змінити електронну адресу + install: Встановлення Answer + upgrade: Оновлення Answer + maintenance: Технічне обслуговування сайту + users: Користувачі + oauth_callback: Обробка + http_404: Помилка HTTP 404 + http_50X: Помилка HTTP 500 + http_403: Помилка HTTP 403 + logout: Вийти + notifications: + title: Сповіщення + inbox: Вхідні + achievement: Досягнення + new_alerts: Нові сповіщення + all_read: Позначити все як прочитане + show_more: Показати більше + someone: Хтось + inbox_type: + all: Усі + posts: Публікації + invites: Запрошення + votes: Голоси + answer: Відповідь + question: Запитання + badge_award: Значок + suspended: + title: Ваш обліковий запис було призупинено + until_time: "Ваш обліковий запис призупинено до {{ time }}." + forever: Цього користувача призупинено назавжди. + end: Ви не дотримуєтеся правил спільноти. + contact_us: Зв'яжіться з нами + editor: + blockquote: + text: Блок Цитування + bold: + text: Надійний + chart: + text: Діаграма + flow_chart: Блок-схема + sequence_diagram: Діаграма послідовності + class_diagram: Діаграма класів + state_diagram: Діаграма станів + entity_relationship_diagram: Діаграма зв'язків сутностей + user_defined_diagram: Визначена користувачем діаграма + gantt_chart: Діаграма Ґанта + pie_chart: Кругова діаграма + code: + text: Зразок коду + add_code: Додати зразок коду + form: + fields: + code: + label: Код + msg: + empty: Код не може бути порожнім. + language: + label: Мова + placeholder: Автоматичне визначення + btn_cancel: Скасувати + btn_confirm: Додати + formula: + text: Формула + options: + inline: Вбудована формула + block: Формула блоку + heading: + text: Заголовок + options: + h1: Заголовок 1 + h2: Заголовок 2 + h3: Заголовок 3 + h4: Заголовок 4 + h5: Заголовок 5 + h6: Заголовок 6 + help: + text: Допомога + hr: + text: Горизонтальна лінійка + image: + text: Зображення + add_image: Додати зображення + tab_image: Завантажити зображення + form_image: + fields: + file: + label: Файл зображення + btn: Обрати зображення + msg: + empty: Файл не може бути порожнім. + only_image: Допустимі лише файли зображень. + max_size: Розмір файлу не може перевищувати {{size}} МБ. + desc: + label: Опис + tab_url: URL зображення + form_url: + fields: + url: + label: URL зображення + msg: + empty: URL-адреса зображення не може бути пустою. + name: + label: Опис + btn_cancel: Скасувати + btn_confirm: Додати + uploading: Завантаження + indent: + text: Абзац + outdent: + text: Відступ + italic: + text: Акцент + link: + text: Гіперпосилання + add_link: Додати гіперпосилання + form: + fields: + url: + label: URL + msg: + empty: URL-адреса не може бути пустою. + name: + label: Опис + btn_cancel: Скасувати + btn_confirm: Додати + ordered_list: + text: Нумерований список + unordered_list: + text: Маркований список + table: + text: Таблиця + heading: Заголовок + cell: Клітинка + file: + text: Прикріпити файли + not_supported: "Не підтримується цей тип файлу. Спробуйте ще раз з {{file_type}}." + max_size: "Розмір прикріплених файлів не може перевищувати {{size}} МБ." + close_modal: + title: Я закриваю цей пост, оскільки... + btn_cancel: Скасувати + btn_submit: Надіслати + remark: + empty: Не може бути порожнім. + msg: + empty: Будь ласка, оберіть причину. + report_modal: + flag_title: Я ставлю відмітку, щоб повідомити про цю публікацію як... + close_title: Я закриваю цей пост, оскільки... + review_question_title: Переглянути питання + review_answer_title: Переглянути відповідь + review_comment_title: Переглянути коментар + btn_cancel: Скасувати + btn_submit: Надіслати + remark: + empty: Не може бути порожнім. + msg: + empty: Будь ласка, оберіть причину. + not_a_url: Формат URL неправильний. + url_not_match: Походження URL не збігається з поточним вебсайтом. + tag_modal: + title: Створити новий теґ + form: + fields: + display_name: + label: Ім'я для відображення + msg: + empty: Ім'я для відображення не може бути порожнім. + range: Ім'я для відображення до 35 символів. + slug_name: + label: Скорочена URL-адреса + desc: Скорочення URL до 35 символів. + msg: + empty: Скорочення URL не може бути пустим. + range: Скорочення URL до 35 символів. + character: Скорочення URL містить незадовільний набір символів. + desc: + label: Опис + revision: + label: Редакція + edit_summary: + label: Підсумок редагування + placeholder: >- + Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) + btn_cancel: Скасувати + btn_submit: Надіслати + btn_post: Опублікувати новий теґ + tag_info: + created_at: Створено + edited_at: Відредаговано + history: Історія + synonyms: + title: Синоніми + text: Наступні теги буде змінено на + empty: Синонімів не знайдено. + btn_add: Додати синонім + btn_edit: Редагувати + btn_save: Зберегти + synonyms_text: Наступні теги буде змінено на + delete: + title: Видалити цей теґ + tip_with_posts: >- +

Ми не дозволяємо видаляти тег з дописами.

Передусім, будь ласка, вилучіть цей тег з дописів.

+ tip_with_synonyms: >- +

Ми не дозволяємо видаляти тег із синонімами.

Передусім, будь ласка, вилучіть синоніми з цього тега.

+ tip: Ви впевнені, що хочете видалити? + close: Закрити + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Редагувати теґ + default_reason: Редагувати теґ + default_first_reason: Додати теґ + btn_save_edits: Зберегти зміни + btn_cancel: Скасувати + dates: + long_date: МММ Д + long_date_with_year: "МММ Д, РРРР" + long_date_with_time: "МММ Д, РРРР [о] ГГ:хв" + now: зараз + x_seconds_ago: "{{count}}сек назад" + x_minutes_ago: "{{count}}хв назад" + x_hours_ago: "{{count}}год назад" + hour: година + day: день + hours: годин + days: дні + month: month + months: months + year: year + reaction: + heart: серце + smile: посмішка + frown: насупився + btn_label: додавати або вилучати реакції + undo_emoji: скасувати реакцію {{ emoji }} + react_emoji: реагувати з {{ emoji }} + unreact_emoji: не реагувати з {{ emoji }} + comment: + btn_add_comment: Додати коментар + reply_to: Відповісти на + btn_reply: Відповісти + btn_edit: Редагувати + btn_delete: Видалити + btn_flag: Відмітити + btn_save_edits: Зберегти зміни + btn_cancel: Скасувати + show_more: "Ще {{count}} коментарів" + tip_question: >- + Використовуйте коментарі, щоб попросити більше інформації або запропонувати покращення. Уникайте відповідей на питання в коментарях. + tip_answer: >- + Використовуйте коментарі, щоб відповідати іншим користувачам або повідомляти їх про зміни. Якщо ви додаєте нову інформацію, відредагуйте свою публікацію, а не коментуйте. + tip_vote: Це додає щось корисне до допису + edit_answer: + title: Редагувати відповідь + default_reason: Редагувати відповідь + default_first_reason: Додати відповідь + form: + fields: + revision: + label: Редакція + answer: + label: Відповідь + feedback: + characters: вміст має бути не менше 6 символів. + edit_summary: + label: Редагувати підсумок + placeholder: >- + Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) + btn_save_edits: Зберегти зміни + btn_cancel: Скасувати + tags: + title: Теґи + sort_buttons: + popular: Популярне + name: Назва + newest: Найновіші + button_follow: Підписатися + button_following: Підписані + tag_label: запитання + search_placeholder: Фільтрувати за назвою теґу + no_desc: Цей теґ не має опису. + more: Більше + wiki: Вікі + ask: + title: Create Question + edit_title: Редагувати питання + default_reason: Редагувати питання + default_first_reason: Create question + similar_questions: Подібні питання + form: + fields: + revision: + label: Редакція + title: + label: Назва + placeholder: What's your topic? Be specific. + msg: + empty: Назва не може бути порожньою. + range: Назва до 150 символів + body: + label: Тіло + msg: + empty: Тіло не може бути порожнім. + tags: + label: Теґи + msg: + empty: Теґи не можуть бути порожніми. + answer: + label: Відповідь + msg: + empty: Відповідь не може бути порожньою. + edit_summary: + label: Редагувати підсумок + placeholder: >- + Коротко поясніть ваші зміни (виправлена орфографія, виправлена граматика, покращене форматування) + btn_post_question: Опублікуйте своє запитання + btn_save_edits: Зберегти зміни + answer_question: Відповісти на власне питання + post_question&answer: Опублікуйте своє запитання і відповідь + tag_selector: + add_btn: Додати теґ + create_btn: Створити новий теґ + search_tag: Шукати теґ + hint: "Describe what your content is about, at least one tag is required." + no_result: Не знайдено тегів + tag_required_text: Обов'язковий тег (принаймні один) + header: + nav: + question: Запитання + tag: Теґи + user: Користувачі + badges: Значки + profile: Профіль + setting: Налаштування + logout: Вийти + admin: Адмін + review: Огляд + bookmark: Закладки + moderation: Модерація + search: + placeholder: Пошук + footer: + build_on: >- + Працює на основі <1> Apache Answer - програмного забезпечення з відкритим вихідним кодом, яке забезпечує роботу спільнот запитань та відповідей.
Зроблено з любов'ю © {{cc}}. + upload_img: + name: Змінити + loading: завантаження... + pic_auth_code: + title: Капча + placeholder: Введіть текст вище + msg: + empty: Капча не може бути порожньою. + inactive: + first: >- + Ви майже закінчили! Ми надіслали лист для активації на {{mail}}. Будь ласка, дотримуйтесь інструкцій, щоб активувати свій обліковий запис. + info: "Якщо він не надійшов, перевірте папку зі спамом." + another: >- + Ми надіслали вам інший електронний лист для активації на {{mail}}. Це може зайняти кілька хвилин, перш ніж він прибуде; обов'язково перевірте теку зі спамом. + btn_name: Повторно надіслати електронний лист для активації + change_btn_name: Змінити електронну пошту + msg: + empty: Не може бути порожнім. + resend_email: + url_label: Ви впевнені, що бажаєте повторно надіслати електронний лист для активації? + url_text: Ви також можете дати користувачеві наведене вище посилання для активації. + login: + login_to_continue: Увійдіть, щоб продовжити + info_sign: Немає облікового запису? <1>Зареєструйтесь + info_login: Вже маєте обліковий запис? <1>Увійдіть + agreements: Реєструючись, ви погоджуєтеся з <1>політикою конфіденційності та <3>умовами використання. + forgot_pass: Забули пароль? + name: + label: Ім’я + msg: + empty: Ім'я не може бути порожнім. + range: Ім'я повинно мати довжину від 2 до 30 символів. + character: 'Необхідно використовувати набір символів "a-z", "A-Z", "0-9", " - . _"' + email: + label: Електронна пошта + msg: + empty: Поле електронної пошти не може бути пустим. + password: + label: Пароль + msg: + empty: Поле паролю не може бути порожнім. + different: Двічі введені паролі є несумісними + account_forgot: + page_title: Забули свій пароль + btn_name: Надішліть мені електронний лист для відновлення + send_success: >- + Якщо обліковий запис збігається з {{mail}}, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля. + email: + label: Електронна пошта + msg: + empty: Поле електронної пошти не може бути пустим. + change_email: + btn_cancel: Скасувати + btn_update: Оновити адресу електронної пошти + send_success: >- + Якщо обліковий запис збігається з {{mail}}, незабаром ви отримаєте електронний лист з інструкціями щодо скидання пароля. + email: + label: Нова електронна пошта + msg: + empty: Поле електронної пошти не може бути пустим. + oauth: + connect: З'єднати з {{ auth_name }} + remove: Видалити {{ auth_name }} + oauth_bind_email: + subtitle: Додайте резервну електронну пошту до свого облікового запису. + btn_update: Оновити адресу електронної пошти + email: + label: Електронна пошта + msg: + empty: Поле електронної пошти не може бути пустим. + modal_title: Електронна адреса вже існує. + modal_content: Ця електронна адреса вже зареєстрована. Ви впевнені, що бажаєте підключитися до існуючого облікового запису? + modal_cancel: Змінити електронну пошту + modal_confirm: Під'єднати до існуючого облікового запису + password_reset: + page_title: Скинути пароль + btn_name: Скинути мій пароль + reset_success: >- + Ви успішно змінили пароль; вас буде перенаправлено на сторінку входу в систему. + link_invalid: >- + На жаль, це посилання для зміни пароля більше недійсне. Можливо, ваш пароль уже скинуто? + to_login: Продовжити вхід на сторінку + password: + label: Пароль + msg: + empty: Поле паролю не може бути порожнім. + length: Довжина повинна бути від 8 до 32 символів + different: Двічі введені паролі є несумісними + password_confirm: + label: Підтвердити новий пароль + settings: + page_title: Налаштування + goto_modify: Перейти до зміни + nav: + profile: Профіль + notification: Сповіщення + account: Обліковий запис + interface: Інтерфейс + profile: + heading: Профіль + btn_name: Зберегти + display_name: + label: Ім'я для відображення + msg: Ім'я для відображення не може бути порожнім. + msg_range: Display name must be 2-30 characters in length. + username: + label: Ім'я користувача + caption: Користувачі можуть згадувати вас як "@username". + msg: Ім’я користувача не може бути порожнім. + msg_range: Username must be 2-30 characters in length. + character: 'Необхідно використовувати набір символів "a-z", "0-9", "-. _"' + avatar: + label: Зображення профілю + gravatar: Gravatar + gravatar_text: Ви можете змінити зображення на + custom: Власне + custom_text: Ви можете завантажити своє зображення. + default: Системне + msg: Будь ласка, завантажте аватар + bio: + label: Про мене + website: + label: Вебсайт + placeholder: "https://example.com" + msg: Неправильний формат вебсайту + location: + label: Місцезнаходження + placeholder: "Місто, Країна" + notification: + heading: Сповіщення електронною поштою + turn_on: Увімкнути + inbox: + label: Вхідні сповіщення + description: Відповіді на ваші запитання, коментарі, запрошення тощо. + all_new_question: + label: Усі нові запитання + description: Отримуйте сповіщення про всі нові питання. До 50 питань на тиждень. + all_new_question_for_following_tags: + label: Всі нові запитання з наступними тегами + description: Отримувати сповіщення про нові запитання з наступними тегами. + account: + heading: Обліковий запис + change_email_btn: Змінити електронну пошту + change_pass_btn: Змінити пароль + change_email_info: >- + Ми надіслали електронний лист на цю адресу. Будь ласка, дотримуйтесь інструкцій для підтвердження. + email: + label: Нова електронна пошта + new_email: + label: Нова електронна пошта + msg: Нова електронна пошта не може бути порожньою. + pass: + label: Поточний пароль + msg: Комірка паролю не може бути порожньою. + password_title: Пароль + current_pass: + label: Поточний пароль + msg: + empty: Комірка поточного пароля не може бути порожньою. + length: Довжина повинна бути від 8 до 32 символів. + different: Два введені паролі не збігаються. + new_pass: + label: Новий пароль + pass_confirm: + label: Підтвердити новий пароль + interface: + heading: Інтерфейс + lang: + label: Мова інтерфейсу + text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку. + my_logins: + title: Мої логіни + label: Увійдіть або зареєструйтеся на цьому сайті, використовуючи ці облікові записи. + modal_title: Видалити логін + modal_content: Ви впевнені, що хочете видалити цей логін з облікового запису? + modal_confirm_btn: Видалити + remove_success: Успішно видалено + toast: + update: успішно оновлено + update_password: Пароль успішно змінено. + flag_success: Дякую, що відмітили. + forbidden_operate_self: Заборонено застосовувати на собі + review: Ваша версія з'явиться після перевірки. + sent_success: Успішно відправлено + related_question: + title: Related + answers: відповіді + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Люди запитували + desc: Виберіть людей, які, на вашу думку, можуть знати відповідь. + invite: Запросити відповісти + add: Додати людей + search: Шукати людей + question_detail: + action: Дія + Asked: Запитали + asked: запитали + update: Змінено + edit: відредаговано + commented: прокоментовано + Views: Переглянуто + Follow: Підписатися + Following: Підписані + follow_tip: Підпишіться на це запитання, щоб отримувати сповіщення + answered: дано відповідь + closed_in: Зачинено в + show_exist: Показати наявне запитання. + useful: Корисне + question_useful: Це корисно і ясно + question_un_useful: Це неясно або некорисно + question_bookmark: Додати в закладки це питання + answer_useful: Це корисно + answer_un_useful: Це некорисно + answers: + title: Відповіді + score: Оцінка + newest: Найновіші + oldest: Найдавніші + btn_accept: Прийняти + btn_accepted: Прийнято + write_answer: + title: Ваша відповідь + edit_answer: Редагувати мою чинну відповідь + btn_name: Опублікувати свою відповідь + add_another_answer: Додати ще одну відповідь + confirm_title: Перейти до відповіді + continue: Продовжити + confirm_info: >- +

Ви впевнені, що хочете додати ще одну відповідь?

Натомість ви можете скористатися посиланням редагування, щоб уточнити та покращити вже існуючу відповідь.

+ empty: Відповідь не може бути порожньою. + characters: вміст має бути не менше 6 символів. + tips: + header_1: Дякуємо за відповідь + li1_1: Будь ласка, не забудьте відповісти на запитання. Надайте детальну інформацію та поділіться своїми дослідженнями. + li1_2: Підкріплюйте будь-які ваші твердження посиланнями чи особистим досвідом. + header_2: Але уникайте... + li2_1: Просити про допомогу, шукати роз'яснення або реагувати на інші відповіді. + reopen: + confirm_btn: Відкрити знову + title: Повторно відкрити цей допис + content: Ви впевнені, що хочете повторно відкрити? + list: + confirm_btn: Список + title: Показати цей допис + content: Ви впевнені, що хочете скласти список? + unlist: + confirm_btn: Вилучити зі списку + title: Вилучити допис зі списку + content: Ви впевнені, що хочете вилучити зі списку? + pin: + title: Закріпити цей допис + content: Ви впевнені, що хочете закріпити глобально? Цей допис відображатиметься вгорі всіх списків публікацій. + confirm_btn: Закріпити + delete: + title: Видалити цей допис + question: >- + Ми не рекомендуємо видаляти питання з відповідями, оскільки це позбавляє майбутніх читачів цих знань.

Повторне видалення запитань із відповідями може призвести до блокування запитів у вашому обліковому записі. Ви впевнені, що хочете видалити? + answer_accepted: >- +

Ми не рекомендуємо видаляти прийняту відповідь, оскільки це позбавляє майбутніх читачів цих знань.

Повторне видалення прийнятих відповідей може призвести до того, що ваш обліковий запис буде заблоковано для відповідей. Ви впевнені, що хочете видалити? + other: Ви впевнені, що хочете видалити? + tip_answer_deleted: Ця відповідь була видалена + undelete_title: Скасувати видалення цього допису + undelete_desc: Ви впевнені, що бажаєте скасувати видалення? + btns: + confirm: Підтвердити + cancel: Скасувати + edit: Редагувати + save: Зберегти + delete: Видалити + undelete: Скасувати видалення + list: Список + unlist: Вилучити зі списку + unlisted: Вилучене зі списку + login: Увійти + signup: Зареєструватися + logout: Вийти + verify: Підтвердити + create: Create + approve: Затвердити + reject: Відхилити + skip: Пропустити + discard_draft: Видалити чернетку + pinned: Закріплено + all: Усі + question: Запитання + answer: Відповідь + comment: Коментар + refresh: Оновити + resend: Надіслати повторно + deactivate: Деактивувати + active: Активні + suspend: Призупинити + unsuspend: Відновити + close: Закрити + reopen: Відкрити знову + ok: ОК + light: Світла + dark: Темна + system_setting: Налаштування системи + default: За замовчуванням + reset: Скинути + tag: Тег + post_lowercase: допис + filter: Фільтр + ignore: Ігнорувати + submit: Надіслати + normal: Нормальний + closed: Закриті + deleted: Видалені + deleted_permanently: Deleted permanently + pending: Очікування + more: Більше + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Результати пошуку + keywords: Ключові слова + options: Параметри + follow: Підписатися + following: Підписані + counts: "{{count}} Результатів" + counts_loading: "... Results" + more: Більше + sort_btns: + relevance: Релевантність + newest: Найновіші + active: Активні + score: Оцінка + more: Більше + tips: + title: Підказки щодо розширеного пошуку + tag: "<1>[tag] шукати за тегом" + user: "<1>користувач:ім'я користувача пошук за автором" + answer: "<1>відповіді:0 питання без відповіді" + score: "<1>рахунок: 3 записи із 3+ рахунком" + question: "<1>є:питання пошукові питання" + is_answer: "<1>є:відповідь пошукові відповіді" + empty: Ми не змогли нічого знайти.
Спробуйте різні або менш конкретні ключові слова. + share: + name: Поділитись + copy: Копіювати посилання + via: Поділитися дописом через... + copied: Скопійовано + facebook: Поділитись на Facebook + twitter: Share to X + cannot_vote_for_self: Ви не можете проголосувати за власну публікацію. + modal_confirm: + title: Помилка... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Ваш новий обліковий запис підтверджено; вас буде перенаправлено на головну сторінку. + link: Перейти на головну сторінку + oops: Йой! + invalid: Посилання, яке ви використовували, більше не працює. + confirm_new_email: Вашу адресу електронної пошти було оновлено. + confirm_new_email_invalid: >- + На жаль, це посилання для підтвердження більше не дійсне. Можливо, ваша електронна пошта вже була змінена? + unsubscribe: + page_title: Відписатися + success_title: Ви успішно відписалися + success_desc: Вас успішно вилучено з цього списку підписників, і ви більше не будете отримувати від нас електронні листи. + link: Змінити налаштування + question: + following_tags: Підписки на теги + edit: Редагувати + save: Зберегти + follow_tag_tip: Підпишіться на теги, щоб упорядкувати свій список запитань. + hot_questions: Гарячі питання + all_questions: Всі питання + x_questions: "{{ count }} Питань" + x_answers: "{{ count }} відповідей" + x_posts: "{{ count }} Posts" + questions: Запитання + answers: Відповіді + newest: Найновіші + active: Активні + hot: Гаряче + frequent: Часто + recommend: Рекомендовано + score: Оцінка + unanswered: Без відповідей + modified: змінено + answered: дано відповідь + asked: запитано + closed: закрито + follow_a_tag: Підписатися на тег + more: Більше + personal: + overview: Загальний огляд + answers: Відповіді + answer: відповідь + questions: Запитання + question: запитання + bookmarks: Закладки + reputation: Репутація + comments: Коментарі + votes: Голоси + badges: Значки + newest: Найновіше + score: Оцінка + edit_profile: Редагувати профіль + visited_x_days: "Відвідано {{ count }} днів" + viewed: Переглянуто + joined: Приєднано + comma: "," + last_login: Переглянуто + about_me: Про мене + about_me_empty: "// Привіт, світ!" + top_answers: Найкращі відповіді + top_questions: Найкращі запитання + stats: Статистика + list_empty: Не знайдено жодного допису.
Можливо, ви хочете вибрати іншу вкладку? + content_empty: Постів не знайдено. + accepted: Прийнято + answered: дано відповідь + asked: запитано + downvoted: проголосовано проти + mod_short: MOD + mod_long: Модератори + x_reputation: репутація + x_votes: отримані голоси + x_answers: відповіді + x_questions: запитання + recent_badges: Нещодавні значки + install: + title: Встановлення + next: Далі + done: Готово + config_yaml_error: Не вдалося створити config.yaml файл. + lang: + label: Будь ласка, виберіть мову + db_type: + label: Рушій бази даних + db_username: + label: Ім'я користувача + placeholder: корінь + msg: Ім’я користувача не може бути порожнім. + db_password: + label: Пароль + placeholder: корінь + msg: Поле паролю не може бути порожнім. + db_host: + label: Хост бази даних + placeholder: "db:3306" + msg: Хост бази даних не може бути порожнім. + db_name: + label: Назва бази даних + placeholder: відповідь + msg: Назва бази даних не може бути порожня. + db_file: + label: Файл бази даних + placeholder: /data/answer.db + msg: Файл бази даних не може бути порожнім. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Створити config.yaml + label: Файл config.yaml створено. + desc: >- + Ви можете створити файл <1>config.yaml вручну в каталозі <1>/var/www/xxx/ і вставити в нього наступний текст. + info: Після цього натисніть кнопку "Далі". + site_information: Інформація про сайт + admin_account: Обліковий запис адміністратора + site_name: + label: Назва сайту + msg: Назва сайту не може бути порожньою. + msg_max_length: Назва сайту повинна містити не більше 30 символів. + site_url: + label: URL сайту + text: Адреса вашого сайту. + msg: + empty: URL-адреса сайту не може бути пустою. + incorrect: Неправильний формат URL-адреси сайту. + max_length: Максимальна довжина URL-адреси сайту – 512 символів. + contact_email: + label: Контактна електронна адреса + text: Електронна адреса основної контактної особи, відповідальної за цей сайт. + msg: + empty: Контактна електронна адреса не може бути порожньою. + incorrect: Неправильний формат контактної електронної пошти. + login_required: + label: Приватний + switch: Вхід обов'язковий + text: Лише авторизовані користувачі можуть отримати доступ до цієї спільноти. + admin_name: + label: Ім’я + msg: Ім'я не може бути порожнім. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Пароль + text: >- + Вам знадобиться цей пароль для входу. Зберігайте його в надійному місці. + msg: Поле паролю не може бути порожнім. + msg_min_length: Пароль має бути не менше 8 символів. + msg_max_length: Пароль має бути не менше 32 символів. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Електронна пошта + text: Вам знадобиться ця електронна адреса для входу. + msg: + empty: Поле електронної пошти не може бути пустим. + incorrect: Невірний формат електронної пошти. + ready_title: Ваш сайт готовий + ready_desc: >- + Якщо ви коли-небудь захочете змінити інші налаштування, відвідайте <1>розділ адміністрування; знайдіть його в меню сайту. + good_luck: "Веселіться, і хай щастить!" + warn_title: Попередження + warn_desc: >- + Файл <1>config.yaml вже існує. Якщо вам потрібно скинути будь-який з елементів конфігурації в цьому файлі, будь ласка, спочатку видаліть його. + install_now: Ви можете спробувати <1>встановити зараз. + installed: Уже встановлено + installed_desc: >- + Ви, здається, уже встановили. Щоб перевстановити, спочатку очистіть старі таблиці бази даних. + db_failed: Не вдалося встановити з'єднання з базою даних + db_failed_desc: >- + Це означає, що інформація про базу даних у вашому файлі <1>config.yaml невірна або що не вдалося встановити контакт із сервером бази даних. Це може означати, що сервер бази даних вашого хоста не працює. + counts: + views: перегляди + votes: голоси + answers: відповіді + accepted: Схвалено + page_error: + http_error: Помилка HTTP {{ code }} + desc_403: Ви не маєте дозволу на доступ до цієї сторінки. + desc_404: На жаль, такої сторінки не існує. + desc_50X: Сервер виявив помилку і не зміг виконати ваш запит. + back_home: Повернутися на головну сторінку + page_maintenance: + desc: "Ми технічно обслуговуємось, ми скоро повернемося." + nav_menus: + dashboard: Панель + contents: Зміст + questions: Питання + answers: Відповіді + users: Користувачі + badges: Значки + flags: Відмітки + settings: Налаштування + general: Основне + interface: Інтерфейс + smtp: SMTP + branding: Брендинг + legal: Правила та умови + write: Написати + tos: Умови використання + privacy: Приватність + seo: SEO + customize: Персоналізувати + themes: Теми + login: Вхід + privileges: Привілеї + plugins: Плагіни + installed_plugins: Встановлені плагіни + apperance: Appearance + website_welcome: Ласкаво просимо до {{site_name}} + user_center: + login: Вхід + qrcode_login_tip: Будь ласка, використовуйте {{ agentName }}, щоб просканувати QR-код і увійти в систему. + login_failed_email_tip: Не вдалося увійти, будь ласка, дозвольте цьому додатку отримати доступ до вашої електронної пошти, перш ніж спробувати ще раз. + badges: + modal: + title: Вітаємо + content: Ти отримав новий значок. + close: Закрити + confirm: Переглянути значки + title: Значки + awarded: Присвоєно + earned_×: Зароблено ×{{ number }} + ×_awarded: "Присвоєно {{ number }}" + can_earn_multiple: Ви можете заробити це багато разів. + earned: Зароблено + admin: + admin_header: + title: Адмін + dashboard: + title: Панель + welcome: Ласкаво просимо до адміністратора! + site_statistics: Статистика сайту + questions: "Запитання:" + resolved: "Вирішено:" + unanswered: "Без відповідей:" + answers: "Відповіді:" + comments: "Коментарі:" + votes: "Голоси:" + users: "Користувачі:" + flags: "Відмітки:" + reviews: "Відгуки:" + site_health: Стан сайту + version: "Версія:" + https: "HTTPS:" + upload_folder: "Завантажити теку:" + run_mode: "Активний режим:" + private: Приватний + public: Публічний + smtp: "SMTP:" + timezone: "Часовий пояс:" + system_info: Інформація про систему + go_version: "Перейти до версії:" + database: "База даних:" + database_size: "Розмір бази даних:" + storage_used: "Використаний обсяг пам’яті:" + uptime: "Час роботи:" + links: Посилання + plugins: Плаґіни + github: GitHub + blog: Блоґ + contact: Контакт + forum: Форум + documents: Документи + feedback: Відгук + support: Підтримка + review: Огляд + config: Конфігурація + update_to: Оновити до + latest: Останній + check_failed: Не вдалося перевірити + "yes": "Так" + "no": "Ні" + not_allowed: Не дозволено + allowed: Дозволено + enabled: Увімкнено + disabled: Вимкнено + writable: Записуваний + not_writable: Не можна записувати + flags: + title: Відмітки + pending: В очікуванні + completed: Завершено + flagged: Відмічено + flagged_type: Відмічено {{ type }} + created: Створені + action: Дія + review: Огляд + user_role_modal: + title: Змінити роль користувача на... + btn_cancel: Скасувати + btn_submit: Надіслати + new_password_modal: + title: Встановити новий пароль + form: + fields: + password: + label: Пароль + text: Користувача буде виведено з системи, і йому потрібно буде увійти знову. + msg: Пароль повинен мати довжину від 8 до 32 символів. + btn_cancel: Скасувати + btn_submit: Надіслати + edit_profile_modal: + title: Редагувати профіль + form: + fields: + display_name: + label: Зображуване ім'я + msg_range: Display name must be 2-30 characters in length. + username: + label: Ім'я користувача + msg_range: Username must be 2-30 characters in length. + email: + label: Електронна пошта + msg_invalid: Невірна адреса електронної пошти. + edit_success: Успішно відредаговано + btn_cancel: Скасувати + btn_submit: Надіслати + user_modal: + title: Додати нового користувача + form: + fields: + users: + label: Масове додавання користувача + placeholder: "Джон Сміт, john@example.com, BUSYopr2\nАліса, alice@example.com, fpDntV8q" + text: '“Ім''я, електронну пошту, пароль” розділити комами. Один користувач у рядку.' + msg: "Будь ласка, введіть електронну пошту користувача, по одній на рядок." + display_name: + label: Ім'я для відображення + msg: Ім'я для показу повинно мати довжину від 2 до 30 символів. + email: + label: Електронна пошта + msg: Електронна пошта недійсна. + password: + label: Пароль + msg: Пароль повинен мати довжину від 8 до 32 символів. + btn_cancel: Скасувати + btn_submit: Надіслати + users: + title: Користувачі + name: Ім’я + email: Електронна пошта + reputation: Репутація + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Статус + role: Роль + action: Дія + change: Зміна + all: Усі + staff: Персонал + more: Більше + inactive: Неактивні + suspended: Призупинено + deleted: Видалено + normal: Нормальний + Moderator: Модератор + Admin: Адмін + User: Користувач + filter: + placeholder: "Фільтр на ім'я, користувач:id" + set_new_password: Встановити новий пароль + edit_profile: Редагувати профіль + change_status: Змінити статус + change_role: Змінити роль + show_logs: Показати записи журналу + add_user: Додати користувача + deactivate_user: + title: Деактивувати користувача + content: Неактивний користувач повинен повторно підтвердити свою електронну адресу. + delete_user: + title: Видалити цього користувача + content: Ви впевнені, що хочете видалити цього користувача? Це назавжди! + remove: Вилучити їх вміст + label: Видалити всі запитання, відповіді, коментарі тощо. + text: Не позначайте цю опцію, якщо ви хочете лише видалити обліковий запис користувача. + suspend_user: + title: Призупинити цього користувача + content: Призупинений користувач не може увійти в систему. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Запитання + unlisted: Вилучене зі списку + post: Опублікувати + votes: Голоси + answers: Відповіді + created: Створені + status: Статус + action: Дія + change: Зміна + pending: Очікування + filter: + placeholder: "Фільтр за назвою, питання:id" + answers: + page_title: Відповіді + post: Допис + votes: Голоси + created: Створено + status: Статус + action: Дія + change: Зміна + filter: + placeholder: "Фільтр за назвою, відповідь:id" + general: + page_title: Основне + name: + label: Назва сайту + msg: Назва сайту не може бути порожньою. + text: "Назва цього сайту як зазначено у заголовку тегу." + site_url: + label: URL сайту + msg: Url сайту не може бути порожньою. + validate: Будь ласка, введіть дійсну URL. + text: Адреса вашого сайту. + short_desc: + label: Короткий опис сайту + msg: Короткий опис сайту не може бути пустим. + text: "Короткий опис, як використовується в заголовку на головній сторінці." + desc: + label: Опис сайту + msg: Опис сайту не може бути порожнім. + text: "Опишіть цей сайт одним реченням, як у тезі метаопису." + contact_email: + label: Контактна електронна пошта + msg: Контактна електронна пошта не може бути порожньою. + validate: Контактна електронна пошта недійсна. + text: Адреса електронної пошти ключової особи, відповідальної за цей сайт. + check_update: + label: Оновлення програмного забезпечення + text: Автоматично перевіряти оновлення + interface: + page_title: Інтерфейс + language: + label: Мова інтерфейсу + msg: Мова інтерфейсу не може бути пустою. + text: Мова інтерфейсу користувача. Зміниться, коли ви оновите сторінку. + time_zone: + label: Часовий пояс + msg: Часовий пояс не може бути пустим. + text: Виберіть місто в тому ж часовому поясі, що й ви. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: З електронної пошти + msg: Поле з електронної пошти не може бути пустим. + text: Адреса електронної пошти, з якої надсилаються листи. + from_name: + label: Від імені + msg: Поле від імені не може бути пустим. + text: Ім'я, з якого надсилаються електронні листи. + smtp_host: + label: SMTP-хост + msg: SMTP хост не може бути порожнім. + text: Ваш поштовий сервер. + encryption: + label: Шифрування + msg: Поле шифрування не може бути пустим. + text: Для більшості серверів SSL є рекомендованим параметром. + ssl: SSL + tls: TLS + none: Нічого + smtp_port: + label: SMTP порт + msg: SMTP порт має бути числом 1 ~ 65535. + text: Порт на ваш поштовий сервер. + smtp_username: + label: Ім'я користувача SMTP + msg: Ім'я користувача SMTP не може бути порожнім. + smtp_password: + label: Пароль SMTP + msg: Пароль до SMTP не може бути порожнім. + test_email_recipient: + label: Тест отримувачів електронної пошти + text: Вкажіть адресу електронної пошти, на яку будуть надходити тестові надсилання. + msg: Тест отримувачів електронної пошти не вірний + smtp_authentication: + label: Увімкнути автентифікацію + title: SMTP аутентифікація + msg: SMTP аутентифікація не може бути порожньою. + "yes": "Так" + "no": "Ні" + branding: + page_title: Брендинг + logo: + label: Логотип + msg: Логотип не може бути порожнім. + text: Зображення логотипу у верхньому лівому кутку вашого сайту. Використовуйте широке прямокутне зображення з висотою 56 і співвідношенням сторін більше 3:1. Якщо залишити це поле порожнім, буде показано текст заголовка сайту. + mobile_logo: + label: Мобільний логотип + text: Логотип, що використовується на мобільній версії вашого сайту. Використовуйте широке прямокутне зображення висотою 56. Якщо залишити поле порожнім, буде використано зображення з налаштування "логотип". + square_icon: + label: Квадратна іконка + msg: Квадратна іконка не може бути пустою. + text: Зображення, що використовується як основа для іконок метаданих. В ідеалі має бути більшим за 512x512. + favicon: + label: Favicon + text: Іконка для вашого сайту. Для коректної роботи через CDN має бути у форматі png. Буде змінено розмір до 32x32. Якщо залишити порожнім, буде використовуватися "квадратна іконка". + legal: + page_title: Правила та умови + terms_of_service: + label: Умови використання + text: "Ви можете додати вміст про умови використання тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу." + privacy_policy: + label: Політика конфіденційности + text: "Ви можете додати вміст політики конфіденційності тут. Якщо у вас уже є документ, розміщений деінде, надайте тут повну URL-адресу." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Написати + restrict_answer: + title: Відповідь на запис + label: Кожен користувач може написати лише одну відповідь на кожне запитання + text: "Вимкнути, щоб дозволити користувачам писати кілька відповідей на одне і те ж питання, що може призвести до розфокусування відповідей." + recommend_tags: + label: Рекомендовані теги + text: "За замовчуванням рекомендовані теги будуть показані у спадному списку." + msg: + contain_reserved: "рекомендовані теги не можуть містити зарезервовані теги" + required_tag: + title: Встановіть необхідні теги + label: Встановіть “Рекомендовані теги” як необхідні теги + text: "Кожне нове питання повинно мати принаймні один рекомендований тег." + reserved_tags: + label: Зарезервовані теги + text: "Зарезервовані теги можуть використовуватися лише модератором." + image_size: + label: Максимальний розмір зображення (МБ) + text: "Максимальний розмір вивантаженого зображення." + attachment_size: + label: Максимальний розмір вкладення (МБ) + text: "Максимальний розмір вкладених файлів для вивантаження." + image_megapixels: + label: Максимальна кількість мегапікселів зображення + text: "Максимальна кількість мегапікселів, дозволена для зображення." + image_extensions: + label: Дозволені розширення зображень + text: "Список розширень файлів, дозволених для показу зображень, через кому." + attachment_extensions: + label: Авторизовані розширення вкладень + text: "Список дозволених для вивантаження розширень файлів, розділених комами. ПОПЕРЕДЖЕННЯ: Дозвіл на вивантаження може спричинити проблеми з безпекою." + seo: + page_title: SEO + permalink: + label: Постійне посилання + text: Користувацькі структури URL можуть покращити уміння та сумісність з надсиланням посилань. + robots: + label: robots.txt + text: Це назавжди замінить будь-які відповідні налаштування сайту. + themes: + page_title: Теми + themes: + label: Теми + text: Виберіть наявну тему. + color_scheme: + label: Схема кольорів + navbar_style: + label: Navbar background style + primary_color: + label: Основний колір + text: Змінюйте кольори, що використовуються у ваших темах + css_and_html: + page_title: CSS та HTML + custom_css: + label: Користувацький CSS + text: > + + head: + label: Головний + text: > + + header: + label: Заголовок + text: > + + footer: + label: Низ + text: Це вставить перед </body>. + sidebar: + label: Бічна панель + text: Це буде вставлено в бічну панель. + login: + page_title: Увійти + membership: + title: Членство + label: Дозволити нові реєстрації + text: Вимкнути, щоб ніхто не міг створити новий обліковий запис. + email_registration: + title: Реєстрація за електронною поштою + label: Дозволити реєстрацію за електронною поштою + text: Вимкніть, щоб запобігти створенню нових облікових записів через електронну пошту. + allowed_email_domains: + title: Дозволені домени електронної пошти + text: Домени електронної пошти, на які користувачі повинні зареєструвати облікові записи. Один домен у рядку. Ігнорується, якщо порожній. + private: + title: Приватний + label: Вхід обов'язковий + text: Доступ до цієї спільноти мають лише зареєстровані користувачі. + password_login: + title: Вхід через пароль + label: Дозволити вхід через електронну пошту і пароль + text: "ПОПЕРЕДЖЕННЯ: Якщо вимкнути, ви не зможете увійти в систему, якщо раніше не налаштували інший метод входу." + installed_plugins: + title: Встановлені плагіни + plugin_link: Плагіни розширюють і поглиблюють функціональність. Ви можете знайти плагіни у <1>Сховищі плагінів. + filter: + all: Усі + active: Активні + inactive: Неактивні + outdated: Застарілі + plugins: + label: Плагіни + text: Виберіть наявний плагін. + name: Ім’я + version: Версія + status: Статус + action: Дія + deactivate: Деактивувати + activate: Активувати + settings: Налаштування + settings_users: + title: Користувачі + avatar: + label: Аватар за замовчуванням + text: Для користувачів без аватара власного. + gravatar_base_url: + label: Основна URL Gravatar + text: URL бази API постачальника Gravatar. Ігнорується, якщо порожній. + profile_editable: + title: Профіль можна редагувати + allow_update_display_name: + label: Дозволити користувачам змінювати ім'я для відображення + allow_update_username: + label: Дозволити користувачам змінювати своє ім'я користувача + allow_update_avatar: + label: Дозволити користувачам змінювати зображення свого профілю + allow_update_bio: + label: Дозволити користувачам змінювати дані про себе + allow_update_website: + label: Дозволити користувачам змінювати свій вебсайт + allow_update_location: + label: Дозволити користувачам змінювати своє місцеперебування + privilege: + title: Привілеї + level: + label: Рівень репутації необхідний + text: Виберіть репутацію, необхідну для привілеїв + msg: + should_be_number: введення має бути числом + number_larger_1: число має бути рівним або більшим за 1 + badges: + action: Дія + active: Активні + activate: Активувати + all: Усі + awards: Нагороди + deactivate: Деактивувати + filter: + placeholder: Фільтрувати за іменем, значок:id + group: Група + inactive: Неактивні + name: Ім’я + show_logs: Показати записи журналу + status: Статус + title: Значки + form: + optional: (необов'язково) + empty: не може бути порожнім + invalid: недійсне + btn_submit: Зберегти + not_found_props: "Необхідний параметр {{ key }} не знайдено." + select: Вибрати + page_review: + review: Огляд + proposed: запропоновано + question_edit: Редагування питання + answer_edit: Редагування відповіді + tag_edit: Редагування тегу + edit_summary: Редагувати звіт + edit_question: Редагувати питання + edit_answer: Редагувати відповідь + edit_tag: Редагувати тег + empty: Не залишилось завдань огляду. + approve_revision_tip: Ви схвалюєте цю редакцію? + approve_flag_tip: Ви схвалюєте цю відмітку? + approve_post_tip: Ви схвалюєте цей допис? + approve_user_tip: Ви схвалюєте цього користувача? + suggest_edits: Запропоновані зміни + flag_post: Відмітити публікацію + flag_user: Відмітити користувача + queued_post: Черговий допис + queued_user: Черговий користувач + filter_label: Тип + reputation: репутація + flag_post_type: Відмічено цей пост як {{ type }}. + flag_user_type: Відмічено цього користувача як {{ type }}. + edit_post: Редагувати допис + list_post: Додати допис до списку + unlist_post: Видалити допис зі списку + timeline: + undeleted: не видалений + deleted: видалений + downvote: голос "проти" + upvote: голос "за" + accept: прийняти + cancelled: скасовано + commented: прокоментовано + rollback: відкат назад + edited: відредаговано + answered: дано відповідь + asked: запитано + closed: закрито + reopened: знову відкрито + created: створено + pin: закріплено + unpin: відкріплено + show: додано до списку + hide: не внесено до списку + title: "Історія для" + tag_title: "Хронологія для" + show_votes: "Показати голоси" + n_or_a: Н/Д + title_for_question: "Хронологія для" + title_for_answer: "Часова шкала для відповіді на {{ title }} від {{ author }}" + title_for_tag: "Часова шкала для тега" + datetime: Дата й час + type: Тип + by: Від + comment: Коментар + no_data: "Ми не змогли нічого знайти." + users: + title: Користувачі + users_with_the_most_reputation: Користувачі з найвищою репутацією на цьому тижні + users_with_the_most_vote: Користувачі, які голосували за найбільше цього тижня + staffs: Персонал нашої спільноти + reputation: репутація + votes: голоси + prompt: + leave_page: Ви дійсно хочете покинути сторінку? + changes_not_save: Ваші зміни можуть не зберегтися. + draft: + discard_confirm: Ви дійсно бажаєте скасувати чернетку? + messages: + post_deleted: Цей допис було видалено. + post_cancel_deleted: Цей допис було не видалено. + post_pin: Цей допис було закріплено. + post_unpin: Цей допис було відкріплено. + post_hide_list: Цей допис було приховано зі списку. + post_show_list: Цей допис було показано у списку. + post_reopen: Цей допис було знову відкрито. + post_list: Цей допис було додано до списку. + post_unlist: Цей допис було приховано. + post_pending: Ваш допис очікує на розгляд. Це попередній перегляд, його буде видно після того, як його буде схвалено. + post_closed: Ця публікація була закрита. + answer_deleted: Ця відповідь була видалена. + answer_cancel_deleted: Ця відповідь була не видалена. + change_user_role: Роль цього користувача було змінено. + user_inactive: Цей користувач вже неактивний. + user_normal: Цей користувач вже нормальний. + user_suspended: Цього користувача було відсторонено. + user_deleted: Цього користувача було видалено. + badge_activated: Цей бейдж було активовано. + badge_inactivated: Цей бейдж було деактивовано. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/vi_VN.yaml b/i18n/vi_VN.yaml new file mode 100644 index 000000000..6bebd382f --- /dev/null +++ b/i18n/vi_VN.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: Thành công. + unknown: + other: Lỗi không xác định. + request_format_error: + other: Định dạng yêu cầu không hợp lệ. + unauthorized_error: + other: Chưa được cấp quyền. + database_error: + other: Lỗi dữ liệu máy chủ. + forbidden_error: + other: Bị cấm. + duplicate_request_error: + other: Trùng lặp yêu cầu. + action: + report: + other: Gắn nhãn + edit: + other: Chỉnh sửa + delete: + other: Xóa + close: + other: Đóng + reopen: + other: Mở lại + forbidden_error: + other: Bị cấm. + pin: + other: Ghim + hide: + other: Gỡ bỏ khỏi danh sách + unpin: + other: Bỏ ghim + show: + other: Hiển thị + invite_someone_to_answer: + other: Chỉnh sửa + undelete: + other: Khôi phục + merge: + other: Merge + role: + name: + user: + other: Người dùng + admin: + other: Quản trị viên + moderator: + other: Người điều hành + description: + user: + other: Mặc định không có quyền truy cập đặc biệt. + admin: + other: Có toàn quyền truy cập vào trang. + moderator: + other: Có quyền truy cập vào tất cả bài viết trừ cài đặt quản trị. + privilege: + level_1: + description: + other: Cấp độ 1 (yêu cầu danh tiếng thấp cho nhóm riêng, nhóm) + level_2: + description: + other: Cấp độ 2 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển) + level_3: + description: + other: Cấp độ 3 (yêu cầu danh tiếng cao cho cộng đồng đã phát triển) + level_custom: + description: + other: Cấp độ tùy chỉnh + rank_question_add_label: + other: Đặt câu hỏi + rank_answer_add_label: + other: Viết câu trả lời + rank_comment_add_label: + other: Viết bình luận + rank_report_add_label: + other: Gắn Cờ + rank_comment_vote_up_label: + other: Bình chọn lên cho bình luận + rank_link_url_limit_label: + other: Đăng nhiều hơn 2 liên kết cùng một lúc + rank_question_vote_up_label: + other: Bình chọn lên cho câu hỏi + rank_answer_vote_up_label: + other: Bình chọn lên cho câu trả lời + rank_question_vote_down_label: + other: Bình chọn xuống cho câu hỏi + rank_answer_vote_down_label: + other: Bình chọn xuống cho câu trả lời + rank_invite_someone_to_answer_label: + other: Mời ai đó trả lời + rank_tag_add_label: + other: Tạo thẻ mới + rank_tag_edit_label: + other: Chỉnh sửa mô tả thẻ (cần xem xét) + rank_question_edit_label: + other: Chỉnh sửa câu hỏi của người khác (cần xem xét) + rank_answer_edit_label: + other: Chỉnh sửa câu trả lời của người khác (cần xem xét) + rank_question_edit_without_review_label: + other: Chỉnh sửa câu hỏi của người khác không cần xem xét + rank_answer_edit_without_review_label: + other: Chỉnh sửa câu trả lời của người khác không cần xem xét + rank_question_audit_label: + other: Xem xét chỉnh sửa câu hỏi + rank_answer_audit_label: + other: Xem xét chỉnh sửa câu trả lời + rank_tag_audit_label: + other: Xem xét chỉnh sửa thẻ + rank_tag_edit_without_review_label: + other: Chỉnh sửa mô tả thẻ không cần xem xét + rank_tag_synonym_label: + other: Quản lý từ đồng nghĩa của thẻ + email: + other: Email + e_mail: + other: Email + password: + other: Mật khẩu + pass: + other: Mật khẩu + old_pass: + other: Current password + original_text: + other: Bài viết này + email_or_password_wrong_error: + other: Email và mật khẩu không trùng khớp. + error: + common: + invalid_url: + other: URL không tồn tại. + status_invalid: + other: Trạng thái không hợp lệ + password: + space_invalid: + other: Mật khẩu không thể tồn tại khoảng trắng. + admin: + cannot_update_their_password: + other: Bạn không thể thay đổi mật khẩu. + cannot_edit_their_profile: + other: Bạn không thể thay đổi hồ sơ. + cannot_modify_self_status: + other: Bạn không thể thay đổi trạng thái của mình. + email_or_password_wrong: + other: Email và mật khẩu không khớp. + answer: + not_found: + other: Không tìm thấy câu trả lời. + cannot_deleted: + other: Không có quyền xóa. + cannot_update: + other: Không có quyền cập nhật. + question_closed_cannot_add: + other: Câu hỏi đã đóng và không thể thêm. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: Không được phép chỉnh sửa bình luận. + not_found: + other: Không tìm thấy bình luận. + cannot_edit_after_deadline: + other: Thời gian bình luận đã quá lâu để chỉnh sửa. + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: Email đã được dùng. + need_to_be_verified: + other: Email cần được xác minh. + verify_url_expired: + other: URL xác minh email đã hết hạn, vui lòng gửi lại email. + illegal_email_domain_error: + other: Email không được phép từ miền email đó. Vui lòng sử dụng miền khác. + lang: + not_found: + other: Không tìm thấy file ngôn ngữ. + object: + captcha_verification_failed: + other: Xác minh Captcha thất bại. + disallow_follow: + other: Bạn không được phép theo dõi. + disallow_vote: + other: Bạn không được phép bỏ phiếu. + disallow_vote_your_self: + other: Bạn không thể bỏ phiếu cho bài đăng của chính mình. + not_found: + other: Đối tượng không tìm thấy. + verification_failed: + other: Xác thực không thành công. + email_or_password_incorrect: + other: Email và mật khẩu không trùng khớp. + old_password_verification_failed: + other: Xác minh mật khẩu cũ thất bại. + new_password_same_as_previous_setting: + other: Mật khẩu mới giống như cài đặt trước. + already_deleted: + other: Mật khẩu mới giống như cài đặt trước. + meta: + object_not_found: + other: Đối tượng không tìm thấy + question: + already_deleted: + other: Bài đăng này đã bị xóa. + under_review: + other: Bài đăng của bạn đang chờ xem xét. Nó sẽ hiển thị sau khi được phê duyệt. + not_found: + other: Không tìm thấy câu hỏi. + cannot_deleted: + other: Không có quyền xóa. + cannot_close: + other: Không có quyền đóng. + cannot_update: + other: Không có quyền cập nhật. + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Xếp hạng danh tiếng không đạt được điều kiện. + vote_fail_to_meet_the_condition: + other: Cảm ơn phản hồi của bạn. Bạn cần ít nhất {{.Rank}} danh tiếng để bỏ phiếu. + no_enough_rank_to_operate: + other: Bạn cần ít nhất {{.Rank}} danh tiếng để làm điều này. + report: + handle_failed: + other: Xử lý báo cáo thất bại. + not_found: + other: Không tìm thấy báo cáo. + tag: + already_exist: + other: Thẻ đã tồn tại. + not_found: + other: Không tìm thấy thẻ. + recommend_tag_not_found: + other: Thẻ đề xuất không tồn tại. + recommend_tag_enter: + other: Vui lòng nhập ít nhất một thẻ bắt buộc. + not_contain_synonym_tags: + other: Không nên chứa các thẻ đồng nghĩa. + cannot_update: + other: Không có quyền cập nhật. + is_used_cannot_delete: + other: Bạn không thể xóa thẻ đang được sử dụng. + cannot_set_synonym_as_itself: + other: Bạn không thể đặt từ đồng nghĩa của thẻ hiện tại là chính nó. + smtp: + config_from_name_cannot_be_email: + other: Tên người gửi không thể là địa chỉ email. + theme: + not_found: + other: Chủ đề không tìm thấy. + revision: + review_underway: + other: Không thể chỉnh sửa hiện tại, có một phiên bản đang trong hàng đợi xem xét. + no_permission: + other: Không có quyền sửa đổi. + user: + external_login_missing_user_id: + other: Nền tảng bên thứ ba không cung cấp UserID duy nhất, vì vậy bạn không thể đăng nhập, vui lòng liên hệ với quản trị viên trang web. + external_login_unbinding_forbidden: + other: Vui lòng đặt mật khẩu đăng nhập cho tài khoản của bạn trước khi bạn gỡ bỏ đăng nhập này. + email_or_password_wrong: + other: + other: Email và mật khẩu không khớp. + not_found: + other: Không tìm thấy người dùng. + suspended: + other: Người dùng đã bị đình chỉ. + username_invalid: + other: Tên người dùng không hợp lệ. + username_duplicate: + other: Tên người dùng đã được sử dụng. + set_avatar: + other: Thiết lập hình đại diện thất bại. + cannot_update_your_role: + other: Bạn không thể sửa đổi vai trò của mình. + not_allowed_registration: + other: Hiện tại trang không mở đăng ký. + not_allowed_login_via_password: + other: Hiện tại trang không cho phép đăng nhập qua mật khẩu. + access_denied: + other: Truy cập bị từ chối + page_access_denied: + other: Bạn không có quyền truy cập trang này. + add_bulk_users_format_error: + other: "Lỗi định dạng {{.Field}} gần '{{.Content}}' tại dòng {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "Số lượng người dùng bạn thêm cùng một lúc nên nằm trong khoảng từ 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: Đọc cấu hình thất bại + database: + connection_failed: + other: Kết nối cơ sở dữ liệu thất bại + create_table_failed: + other: Tạo bảng thất bại + install: + create_config_failed: + other: Không thể tạo file config.yaml. + upload: + unsupported_file_format: + other: Định dạng tệp không được hỗ trợ. + site_info: + config_not_found: + other: Không tìm thấy cấu hình trang. + badge: + object_not_found: + other: Đối tượng không tìm thấy + reason: + spam: + name: + other: thư rác + desc: + other: Bài đăng này quảng cáo hoặc phá hoại. Nó không hữu ích hoặc liên quan đến chủ đề hiện tại. + rude_or_abusive: + name: + other: thô lỗ hoặc lạm dụng + desc: + other: "Một người hợp lý sẽ thấy nội dung này không phù hợp để diễn thuyết một cách tôn trọng." + a_duplicate: + name: + other: một bản sao + desc: + other: Câu hỏi này đã được hỏi trước đó, đã có câu trả lời. + placeholder: + other: Nhập liên kết câu hỏi hiện tại + not_a_answer: + name: + other: không phải câu trả lời + desc: + other: "Điều này đã được đăng dưới dạng câu trả lời nhưng nó không cố gắng trả lời câu hỏi. Nó có thể là một bản chỉnh sửa, một nhận xét, một câu hỏi khác hoặc bị xóa hoàn toàn." + no_longer_needed: + name: + other: không còn cần thiết + desc: + other: Bình luận này đã lỗi thời, đối thoại hoặc không liên quan đến bài đăng này. + something: + name: + other: điều gì đó khác + desc: + other: Bài đăng này cần sự chú ý của nhân viên vì một lý do khác không được liệt kê ở trên. + placeholder: + other: Hãy cho chúng tôi biết cụ thể điều gì bạn quan tâm + community_specific: + name: + other: một lý do cụ thể của cộng đồng + desc: + other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng. + not_clarity: + name: + other: cần chi tiết hoặc rõ ràng + desc: + other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó nên tập trung vào một vấn đề duy nhất. + looks_ok: + name: + other: trông ổn + desc: + other: Bài đăng này tốt như vậy và không kém chất lượng. + needs_edit: + name: + other: cần chỉnh sửa, và tôi đã làm điều đó + desc: + other: Cải thiện và sửa các vấn đề với bài đăng này bằng chính bạn. + needs_close: + name: + other: cần đóng + desc: + other: Một câu hỏi đã đóng không thể trả lời, nhưng vẫn có thể chỉnh sửa, bỏ phiếu và bình luận. + needs_delete: + name: + other: cần xóa + desc: + other: Bài đăng này sẽ bị xóa. + question: + close: + duplicate: + name: + other: spam + desc: + other: Câu hỏi này đã được hỏi trước đó và đã có câu trả lời. + guideline: + name: + other: một lý do cụ thể của cộng đồng + desc: + other: Câu hỏi này không đáp ứng hướng dẫn của cộng đồng. + multiple: + name: + other: cần chi tiết hoặc rõ ràng + desc: + other: Câu hỏi này hiện bao gồm nhiều câu hỏi trong một. Nó chỉ nên tập trung vào một vấn đề. + other: + name: + other: điều gì đó khác + desc: + other: Bài đăng này cần một lý do khác không được liệt kê ở trên. + operation_type: + asked: + other: đã hỏi + answered: + other: đã trả lời + modified: + other: đã chỉnh sửa + deleted_title: + other: Câu hỏi đã xóa + questions_title: + other: Các câu hỏi + tag: + tags_title: + other: Thẻ + no_description: + other: Thẻ không có mô tả. + notification: + action: + update_question: + other: câu hỏi đã cập nhật + answer_the_question: + other: đã trả lời câu hỏi + update_answer: + other: câu trả lời đã cập nhật + accept_answer: + other: câu trả lời đã chấp nhận + comment_question: + other: đã bình luận câu hỏi + comment_answer: + other: đã bình luận câu trả lời + reply_to_you: + other: đã trả lời bạn + mention_you: + other: đã nhắc đến bạn + your_question_is_closed: + other: Câu hỏi của bạn đã được đóng + your_question_was_deleted: + other: Câu hỏi của bạn đã bị xóa + your_answer_was_deleted: + other: Câu trả lời của bạn đã bị xóa + your_comment_was_deleted: + other: Bình luận của bạn đã bị xóa + up_voted_question: + other: câu hỏi đã bình chọn lên + down_voted_question: + other: câu hỏi đã bình chọn xuống + up_voted_answer: + other: câu trả lời đã bình chọn lên + down_voted_answer: + other: câu trả lời đã bình chọn xuống + up_voted_comment: + other: bình luận đã bình chọn lên + invited_you_to_answer: + other: đã mời bạn trả lời + earned_badge: + other: Bạn đã nhận được huy hiệu "{{.BadgeName}}" + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Xác nhận địa chỉ email mới của bạn" + body: + other: "Xác nhận địa chỉ email mới của bạn cho {{.SiteName}} bằng cách nhấp vào liên kết sau:
\n{{.ChangeEmailUrl}}

\n\nNếu bạn không yêu cầu thay đổi này, vui lòng bỏ qua email này.

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} đã trả lời câu hỏi của bạn" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} mời bạn trả lời" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
Tôi nghĩ bạn có thể biết câu trả lời.

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} đã bình luận về bài đăng của bạn" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nXem trên {{.SiteName}}

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời thư này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn.

\n\nHủy đăng ký" + new_question: + title: + other: "[{{.SiteName}}] Câu hỏi mới: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName}}] Đặt lại mật khẩu" + body: + other: "Ai đó đã yêu cầu đặt lại mật khẩu của bạn trên {{.SiteName}}.

\n\nNếu người đó không phải là bạn thì bạn có thể yên tâm bỏ qua email này.

\n\nNhấp vào liên kết sau để chọn mật khẩu mới:
\n{{.PassResetUrl}}\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." + register: + title: + other: "[{{.SiteName}}] Xác nhận tài khoản mới của bạn" + body: + other: "Chào mừng bạn đến với {{.SiteName}}!

\n\nNhấp vào liên kết sau để xác nhận và kích hoạt tài khoản mới của bạn:
\n{{.RegisterUrl}}

\n\nNếu liên kết trên không nhấp vào được, hãy thử sao chép và dán nó vào thanh địa chỉ trình duyệt web của bạn.\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." + test: + title: + other: "[{{.SiteName}}] Email kiểm tra" + body: + other: "Đây là một email thử nghiệm.\n

\n\n--
\nLưu ý: Đây là email hệ thống tự động, vui lòng không trả lời tin nhắn này vì chúng tôi sẽ không nhìn thấy phản hồi của bạn." + action_activity_type: + upvote: + other: bình chọn lên + upvoted: + other: đã bình chọn lên + downvote: + other: bình chọn xuống + downvoted: + other: đã bình chọn xuống + accept: + other: chấp nhận + accepted: + other: đã chấp nhận + edit: + other: chỉnh sửa + review: + queued_post: + other: Bài đăng trong hàng đợi + flagged_post: + other: Bài đăng được đánh dấu + suggested_post_edit: + other: Đề xuất chỉnh sửa + reaction: + tooltip: + other: "{{ .Names }} và {{ .Count }} thêm..." + badge: + default_badges: + autobiographer: + name: + other: Tác giả tự truyện + desc: + other: Đã điền thông tin hồ sơ. + certified: + name: + other: Đã xác minh + desc: + other: Hoàn thành hướng dẫn cho người dùng mới của chúng tôi. + editor: + name: + other: Trình chỉnh sửa + desc: + other: Chỉnh sửa bài đăng đầu tiên. + first_flag: + name: + other: Cờ đầu tiên + desc: + other: Lần đầu tiên báo cáo một bài viết. + first_upvote: + name: + other: Lượt thích đầu tiên + desc: + other: Lần đầu tiên báo cáo một bài viết. + first_link: + name: + other: Liên kết đầu tiên + desc: + other: First added a link to another post. + first_reaction: + name: + other: Phản ứng đầu tiên + desc: + other: Phản ứng với bài viết đầu tiên. + first_share: + name: + other: Chia sẻ đầu tiên + desc: + other: Lần đầu chia sẻ một bài viết. + scholar: + name: + other: Học giả + desc: + other: Đặt một câu hỏi và chấp nhận một câu trả lời. + commentator: + name: + other: Bình luận viên + desc: + other: Để lại 5 bình luận. + new_user_of_the_month: + name: + other: Người dùng mới của tháng + desc: + other: Đóng góp nổi bật trong tháng đầu tiên của họ. + read_guidelines: + name: + other: Đọc hướng dẫn + desc: + other: Đọc [nguyên tắc cộng đồng]. + reader: + name: + other: Người đọc + desc: + other: Đọc mọi câu trả lời trong một chủ đề có hơn 10 câu trả lời. + welcome: + name: + other: Xin chào + desc: + other: Đã nhận được phiếu tán thành. + nice_share: + name: + other: Chia sẻ hay + desc: + other: Đã chia sẻ một bài đăng với 25 khách truy cập. + good_share: + name: + other: Chia sẻ tốt + desc: + other: Đã chia sẻ một bài đăng với 300 khách truy cập. + great_share: + name: + other: Chia sẻ tuyệt vời + desc: + other: Đã chia sẻ một bài đăng với 1000 khách truy cập. + out_of_love: + name: + other: Hết yêu thích + desc: + other: Đã sử dụng 50 phiếu bầu trong một ngày. + higher_love: + name: + other: Thích cao hơn + desc: + other: Đã sử dụng 50 phiếu bầu trong một ngày. + crazy_in_love: + name: + other: Thích điên cuồng + desc: + other: Đã sử dụng 50 phiếu bầu trong một ngày 20 lần. + promoter: + name: + other: Người quảng bá + desc: + other: Đã mời một người dùng. + campaigner: + name: + other: Chiến dịch + desc: + other: Đã mời 3 người dùng cơ bản. + champion: + name: + other: Vô địch + desc: + other: Mời 5 thành viên. + thank_you: + name: + other: Cảm ơn bạn + desc: + other: Có 20 bài đăng được bình chọn đưa ra 10 phiếu bầu. + gives_back: + name: + other: Trả lại + desc: + other: Có 100 bài đăng được bình chọn và đưa ra 100 phiếu bầu. + empathetic: + name: + other: Đồng cảm + desc: + other: Có 500 bài đăng được bình chọn đưa ra 1000 phiếu bầu. + enthusiast: + name: + other: Người nhiệt thành + desc: + other: Đã truy cập 10 ngày liên tiếp. + aficionado: + name: + other: Người hâm mộ + desc: + other: Đã truy cập 100 ngày liên tiếp. + devotee: + name: + other: Tín đồ + desc: + other: Đã truy cập 365 ngày liên tiếp. + anniversary: + name: + other: Kỉ niệm + desc: + other: Thành viên tích cực trong một năm, đăng ít nhất một lần. + appreciated: + name: + other: Đánh giá cao + desc: + other: Nhận được 1 lượt bình chọn cho 20 bài viết. + respected: + name: + other: Tôn trọng + desc: + other: Nhận được 2 lượt bình chọn cho 100 bài viết. + admired: + name: + other: Ngưỡng mộ + desc: + other: Nhận được 5 lượt bình chọn trên 300 bài đăng. + solved: + name: + other: Đã giải quyết + desc: + other: Có một câu trả lời được chấp nhận. + guidance_counsellor: + name: + other: Cố vấn hướng dẫn + desc: + other: Có 10 câu trả lời được chấp nhận. + know_it_all: + name: + other: Biết tất cả + desc: + other: Có 50 câu trả lời được chấp nhận. + solution_institution: + name: + other: Viện giải pháp + desc: + other: Có 150 câu trả lời được chấp nhận. + nice_answer: + name: + other: Câu trả lời tốt + desc: + other: Điểm trả lời từ 10 trở lên. + good_answer: + name: + other: Câu trả lời của bạn + desc: + other: Điểm trả lời từ 25 trở lên. + great_answer: + name: + other: Câu trả lời tuyệt vời + desc: + other: Điểm trả lời từ 50 trở lên. + nice_question: + name: + other: Câu trả lời tốt + desc: + other: Điểm trả lời từ 10 trở lên. + good_question: + name: + other: Câu trả lời tốt + desc: + other: Điểm trả lời từ 25 trở lên. + great_question: + name: + other: Câu trả lời tốt + desc: + other: Điểm trả lời từ 50 trở lên. + popular_question: + name: + other: Câu hỏi phổ biến + desc: + other: Câu hỏi với 500 lượt xem. + notable_question: + name: + other: Câu hỏi đáng chú ý + desc: + other: Câu hỏi với 1.000 lượt xem. + famous_question: + name: + other: Câu hỏi nổi tiếng + desc: + other: Câu hỏi với 5.000 lượt xem. + popular_link: + name: + other: Liên kết phổ biến + desc: + other: Đã đăng một liên kết bên ngoài với 50 lần nhấp chuột. + hot_link: + name: + other: Liên kết nổi bật + desc: + other: Đã đăng một liên kết bên ngoài với 300 lần nhấp chuột. + famous_link: + name: + other: Liên kết nổi tiếng + desc: + other: Đã đăng một liên kết bên ngoài với 100 lần nhấp chuột. + default_badge_groups: + getting_started: + name: + other: Bắt đầu + community: + name: + other: Cộng đồng + posting: + name: + other: Viết bài thảo luận +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: Cách định dạng + desc: >- + + pagination: + prev: Trước + next: Tiếp + page_title: + question: Câu hỏi + questions: Các câu hỏi + tag: Thẻ + tags: Các thẻ + tag_wiki: wiki thẻ + create_tag: Tạo thẻ + edit_tag: Chỉnh sửa thẻ + ask_a_question: Create Question + edit_question: Chỉnh sửa câu hỏi + edit_answer: Chỉnh sửa câu + search: Tìm kiếm + posts_containing: Bài đăng chứa + settings: Cài đặt + notifications: Các thông báo + login: Đăng nhập + sign_up: Đăng ký + account_recovery: Khôi phục tài khoản + account_activation: Kích hoạt tài khoản + confirm_email: Xác nhận Email + account_suspended: Tài khoản bị đình chỉ + admin: Quản trị + change_email: Thay đổi Email + install: Cài đặt Answer + upgrade: Nâng cấp Answer + maintenance: Bảo trì trang web + users: Người dùng + oauth_callback: Đang xử lý + http_404: Lỗi HTTP 404 + http_50X: Lỗi HTTP 500 + http_403: Lỗi HTTP 403 + logout: Đăng xuất + notifications: + title: Các thông báo + inbox: Hộp thư đến + achievement: Thành tích + new_alerts: Cảnh báo mới + all_read: Đánh dấu tất cả đã đọc + show_more: Xem thêm + someone: Ai đó + inbox_type: + all: Tất cả + posts: Bài đăng + invites: Lời mời + votes: Bình chọn + answer: Câu trả lời + question: Câu hỏi + badge_award: Huy hiệu + suspended: + title: Tài khoản của bạn đã bị đình chỉ + until_time: "Tài khoản của bạn đã bị đình chỉ cho đến {{ time }}." + forever: Người dùng này đã bị đình chỉ vĩnh viễn. + end: Bạn không tuân thủ hướng dẫn cộng đồng. + contact_us: Liên hệ với chúng tôi + editor: + blockquote: + text: Trích dẫn + bold: + text: Đậm + chart: + text: Biểu đồ + flow_chart: Biểu đồ luồng + sequence_diagram: Sơ đồ trình tự + class_diagram: Sơ đồ lớp + state_diagram: Sơ đồ trạng thái + entity_relationship_diagram: Sơ đồ quan hệ thực thể + user_defined_diagram: Sơ đồ do người dùng định nghĩa + gantt_chart: Biểu đồ Gantt + pie_chart: Biểu đồ tròn + code: + text: Mẫu code + add_code: Thêm code mẫu + form: + fields: + code: + label: Mã + msg: + empty: Mã không thể trống. + language: + label: Ngôn ngữ + placeholder: Phát hiện tự động + btn_cancel: Hủy + btn_confirm: Thêm + formula: + text: Công thức + options: + inline: Công thức nội dòng + block: Công thức khối + heading: + text: Tiêu đề + options: + h1: Tiêu đề 1 + h2: Tiêu đề 2 + h3: Tiêu đề 3 + h4: Tiêu đề 4 + h5: Tiêu đề 5 + h6: Tiêu đề 6 + help: + text: Trợ giúp + hr: + text: Thước ngang + image: + text: Hình ảnh + add_image: Thêm hình ảnh + tab_image: Tải Ảnh lên + form_image: + fields: + file: + label: Tệp hình ảnh + btn: Chọn hình ảnh + msg: + empty: Tệp không thể trống. + only_image: Chỉ cho phép tệp hình ảnh. + max_size: Kích thước tệp không được vượt quá {{size}} MB. + desc: + label: Mô tả + tab_url: URL hình ảnh + form_url: + fields: + url: + label: URL hình ảnh + msg: + empty: URL hình ảnh không thể trống. + name: + label: Mô tả + btn_cancel: Hủy + btn_confirm: Thêm + uploading: Đang tải lên + indent: + text: Canh lề + outdent: + text: Lùi lề + italic: + text: Nhấn mạnh + link: + text: Liên kết + add_link: Thêm liên kết + form: + fields: + url: + label: Đường link url + msg: + empty: URL không thể trống. + name: + label: Mô tả + btn_cancel: Hủy + btn_confirm: Thêm + ordered_list: + text: Danh sách đánh số + unordered_list: + text: Danh sách gạch đầu dòng + table: + text: Bảng + heading: Tiêu đề + cell: Ô + file: + text: Đính kèm tập tin + not_supported: "Không hỗ trợ loại tệp đó. Hãy thử lại với {{file_type}}." + max_size: "Kích thước tệp đính kèm không được vượt quá {{size}} MB." + close_modal: + title: Tôi đang đóng bài đăng này với lý do... + btn_cancel: Hủy + btn_submit: Gửi + remark: + empty: Không thể trống. + msg: + empty: Vui lòng chọn một lý do. + report_modal: + flag_title: Tôi đang đánh dấu để báo cáo bài đăng này với lý do... + close_title: Tôi đang đóng bài đăng này với lý do... + review_question_title: Xem xét câu hỏi + review_answer_title: Xem xét câu trả lời + review_comment_title: Xem xét bình luận + btn_cancel: Hủy + btn_submit: Gửi + remark: + empty: Không thể trống. + msg: + empty: Vui lòng chọn một lý do. + not_a_url: Định dạng URL không chính xác. + url_not_match: Nguồn gốc URL không khớp với trang web hiện tại. + tag_modal: + title: Tạo thẻ mới + form: + fields: + display_name: + label: Tên hiển thị + msg: + empty: Tên hiển thị không thể trống. + range: Tên hiển thị tối đa 35 ký tự. + slug_name: + label: Đường dẫn URL + desc: Đường dẫn tối đa 35 ký tự. + msg: + empty: Đường dẫn URL không thể trống. + range: Đường dẫn URL tối đa 35 ký tự. + character: Đường dẫn URL chứa bộ ký tự không được phép. + desc: + label: Mô tả + revision: + label: Sửa đổi + edit_summary: + label: Tóm tắt chỉnh sửa + placeholder: >- + Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) + btn_cancel: Hủy + btn_submit: Gửi + btn_post: Đăng thẻ mới + tag_info: + created_at: Đã tạo + edited_at: Đã chỉnh sửa + history: Lịch sử + synonyms: + title: Từ đồng nghĩa + text: Các thẻ sau sẽ được ánh xạ lại thành + empty: Không tìm thấy từ đồng nghĩa. + btn_add: Thêm từ đồng nghĩa + btn_edit: Chỉnh sửa + btn_save: Lưu + synonyms_text: Các thẻ sau sẽ được ánh xạ lại thành + delete: + title: Xóa thẻ này + tip_with_posts: >- +

Chúng tôi không cho phép xóa thẻ có bài đăng.

Vui lòng xóa thẻ này khỏi các bài đăng trước.

+ tip_with_synonyms: >- +

Chúng tôi không cho phép xóa thẻ có từ đồng nghĩa.

Vui lòng xóa các từ đồng nghĩa khỏi thẻ này trước.

+ tip: Bạn có chắc chắn muốn xóa không? + close: Đóng + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: Submit + btn_close: Close + edit_tag: + title: Chỉnh sửa Thẻ + default_reason: Chỉnh sửa thẻ + default_first_reason: Thêm thẻ + btn_save_edits: Lưu chỉnh sửa + btn_cancel: Hủy + dates: + long_date: MMM D + long_date_with_year: "MMM D, YYYY" + long_date_with_time: "MMM D, YYYY [at] HH:mm" + now: bây giờ + x_seconds_ago: "{{count}}giây trước" + x_minutes_ago: "{{count}}phút trước" + x_hours_ago: "{{count}}giờ trước" + hour: giờ + day: ngày + hours: giờ + days: ngày + month: month + months: months + year: year + reaction: + heart: trái tim + smile: nụ cười + frown: nhăn mặt + btn_label: thêm hoặc loại bỏ phản ứng + undo_emoji: bỏ dấu {{ emoji }} phản ứng + react_emoji: biểu cảm với {{ emoji }} + unreact_emoji: hủy biểu cảm {{ emoji }} + comment: + btn_add_comment: Thêm bình luận + reply_to: Trả lời cho + btn_reply: Trả lời + btn_edit: Chỉnh sửa + btn_delete: Xóa + btn_flag: Gắn Cờ + btn_save_edits: Lưu chỉnh sửa + btn_cancel: Hủy + show_more: "{{count}} bình luận khác" + tip_question: >- + Sử dụng bình luận để yêu cầu thêm thông tin hoặc đề xuất cải tiến. Tránh trả lời câu hỏi trong bình luận. + tip_answer: >- + Sử dụng bình luận để trả lời cho người dùng khác hoặc thông báo cho họ về các thay đổi. Nếu bạn đang thêm thông tin mới, hãy chỉnh sửa bài đăng của mình thay vì bình luận. + tip_vote: Nó thêm điều gì đó hữu ích cho bài đăng + edit_answer: + title: Chỉnh sửa Câu trả lời + default_reason: Chỉnh sửa câu trả lời + default_first_reason: Thêm câu trả lời + form: + fields: + revision: + label: Sửa đổi + answer: + label: Câu trả lời + feedback: + characters: nội dung phải có ít nhất 6 ký tự. + edit_summary: + label: Tóm tắt chỉnh sửa + placeholder: >- + Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) + btn_save_edits: Lưu chỉnh sửa + btn_cancel: Hủy + tags: + title: Thẻ + sort_buttons: + popular: Phổ biến + name: Tên + newest: Mới nhất + button_follow: Theo dõi + button_following: Đang theo dõi + tag_label: câu hỏi + search_placeholder: Lọc theo tên thẻ + no_desc: Thẻ không có mô tả. + more: Thêm + wiki: Wiki + ask: + title: Create Question + edit_title: Chỉnh sửa Câu hỏi + default_reason: Chỉnh sửa câu hỏi + default_first_reason: Create question + similar_questions: Câu hỏi tương tự + form: + fields: + revision: + label: Sửa đổi + title: + label: Tiêu đề + placeholder: What's your topic? Be specific. + msg: + empty: Tiêu đề không thể trống. + range: Tiêu đề tối đa 150 ký tự + body: + label: Nội dung + msg: + empty: Nội dung không thể trống. + tags: + label: Thẻ + msg: + empty: Thẻ không thể trống. + answer: + label: Câu trả lời + msg: + empty: Câu trả lời không thể trống. + edit_summary: + label: Tóm tắt chỉnh sửa + placeholder: >- + Giải thích ngắn gọn các thay đổi của bạn (sửa chính tả, sửa ngữ pháp, cải thiện định dạng) + btn_post_question: Đăng câu hỏi của bạn + btn_save_edits: Lưu chỉnh sửa + answer_question: Trả lời câu hỏi của chính bạn + post_question&answer: Đăng câu hỏi và câu trả lời của bạn + tag_selector: + add_btn: Thêm thẻ + create_btn: Tạo thẻ mới + search_tag: Tìm kiếm thẻ + hint: "Describe what your content is about, at least one tag is required." + no_result: Không có thẻ phù hợp + tag_required_text: Thẻ bắt buộc (ít nhất một) + header: + nav: + question: Câu hỏi + tag: Thẻ + user: Người dùng + badges: Danh hiệu + profile: Hồ sơ + setting: Cài đặt + logout: Đăng xuất + admin: Quản trị + review: Xem xét + bookmark: Đánh dấu + moderation: Điều hành + search: + placeholder: Tìm kiếm + footer: + build_on: >- + Được hỗ trợ bởi <1> Apache Answer - phần mềm mã nguồn mở dành cho cộng đồng hỏi đáp.
Được tạo với tình yêu © {{cc}}. + upload_img: + name: Thay đổi + loading: đang tải... + pic_auth_code: + title: Mã xác minh + placeholder: Nhập văn bản ở trên + msg: + empty: Captcha không thể trống. + inactive: + first: >- + Bạn gần như đã hoàn tất! Chúng tôi đã gửi một email kích hoạt đến {{mail}}. Vui lòng làm theo hướng dẫn trong email để kích hoạt tài khoản của bạn. + info: "Nếu không nhận được, hãy kiểm tra thư mục spam của bạn." + another: >- + Chúng tôi đã gửi một email kích hoạt khác cho bạn tại {{mail}}. Có thể mất vài phút để nó đến; hãy chắc chắn kiểm tra thư mục thư rác của bạn. + btn_name: Gửi lại email kích hoạt + change_btn_name: Thay đổi email + msg: + empty: Không thể để trống mục này. + resend_email: + url_label: Bạn có chắc chắn muốn gửi lại email kích hoạt không? + url_text: Bạn cũng có thể cung cấp liên kết kích hoạt ở trên cho người dùng. + login: + login_to_continue: Đăng nhập để tiếp tục + info_sign: Bạn không có tài khoản? <1>Đăng ký + info_login: Bạn đã có tài khoản? <1>Đăng nhập + agreements: Bằng cách đăng ký, bạn đồng ý với <1>chính sách bảo mật và <3>điều khoản dịch vụ. + forgot_pass: Quên mật khẩu? + name: + label: Tên + msg: + empty: Tên không thể trống. + range: Tên phải có độ dài từ 2 đến 30 ký tự. + character: 'Chỉ sử dụng bộ ký tự "a-z", "0-9", , "A-Z", " - . _"' + email: + label: Email + msg: + empty: Email không thể trống. + password: + label: Mật khẩu + msg: + empty: Mật khẩu không thể trống. + different: Mật khẩu nhập vào ở hai bên không nhất quán + account_forgot: + page_title: Quên mật khẩu + btn_name: Gửi email khôi phục cho tôi + send_success: >- + Nếu một tài khoản khớp với {{mail}}, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình. + email: + label: Email + msg: + empty: Email không thể trống. + change_email: + btn_cancel: Hủy + btn_update: Cập nhật địa chỉ email + send_success: >- + Nếu một tài khoản khớp với {{mail}}, bạn sẽ sớm nhận được một email với hướng dẫn về cách đặt lại mật khẩu của mình. + email: + label: Email mới + msg: + empty: Email không thể trống. + oauth: + connect: Kết nối với {{ auth_name }} + remove: Xóa bỏ {{ auth_name }} + oauth_bind_email: + subtitle: Thêm email khôi phục vào tài khoản của bạn. + btn_update: Cập nhật địa chỉ email + email: + label: Email + msg: + empty: Email không thể trống. + modal_title: Email đã tồn tại. + modal_content: Địa chỉ email này đã được đăng ký. Bạn có chắc chắn muốn kết nối với tài khoản hiện tại không? + modal_cancel: Thay đổi email + modal_confirm: Kết nối với tài khoản hiện tại + password_reset: + page_title: Đặt lại mật khẩu + btn_name: Đặt lại mật khẩu của tôi + reset_success: >- + Bạn đã thay đổi mật khẩu thành công; bạn sẽ được chuyển hướng đến trang đăng nhập. + link_invalid: >- + Xin lỗi, liên kết đặt lại mật khẩu này không còn hợp lệ. Có thể mật khẩu của bạn đã được đặt lại? + to_login: Tiếp tục đến trang đăng nhập + password: + label: Mật khẩu + msg: + empty: Mật khẩu không thể trống. + length: Độ dài cần nằm trong khoảng từ 8 đến 32 + different: Mật khẩu nhập vào ở hai bên không nhất quán + password_confirm: + label: Xác nhận mật khẩu mới + settings: + page_title: Cài đặt + goto_modify: Đi đến sửa đổi + nav: + profile: Hồ sơ + notification: Thông báo + account: Tài khoản + interface: Giao diện + profile: + heading: Hồ sơ + btn_name: Lưu + display_name: + label: Tên hiển thị + msg: Tên hiển thị không thể trống. + msg_range: Display name must be 2-30 characters in length. + username: + label: Tên người dùng + caption: Mọi người có thể nhắc đến bạn với "@username". + msg: Tên người dùng không thể trống. + msg_range: Username must be 2-30 characters in length. + character: 'Chỉ sử dụng bộ ký tự "a-z", "0-9", " - . _"' + avatar: + label: Hình ảnh hồ sơ + gravatar: Gravatar + gravatar_text: Bạn có thể thay đổi hình ảnh trên + custom: Tùy chỉnh + custom_text: Bạn có thể tải lên hình ảnh của mình. + default: Hệ thống + msg: Vui lòng tải lên một hình đại diện + bio: + label: Giới thiệu về tôi + website: + label: Website + placeholder: "https://example.com" + msg: Định dạng website không chính xác + location: + label: Địa điểm + placeholder: "Thành phố, Quốc gia" + notification: + heading: Thông báo qua Email + turn_on: Bật + inbox: + label: Thông báo hộp thư đến + description: Các câu trả lời cho câu hỏi của bạn, bình luận, lời mời và nhiều hơn nữa. + all_new_question: + label: Tất cả câu hỏi mới + description: Nhận thông báo về tất cả các câu hỏi mới. Tối đa 50 câu hỏi mỗi tuần. + all_new_question_for_following_tags: + label: Tất cả câu hỏi mới cho các thẻ theo dõi + description: Nhận thông báo về các câu hỏi mới cho các thẻ đang theo dõi. + account: + heading: Tài khoản + change_email_btn: Thay đổi email + change_pass_btn: Thay đổi mật khẩu + change_email_info: >- + Chúng tôi đã gửi một email đến địa chỉ đó. Vui lòng làm theo hướng dẫn xác nhận. + email: + label: Email + new_email: + label: Email mới + msg: Email mới không được để trống. + pass: + label: Mật khẩu hiện tại + msg: Mật khẩu không thể trống. + password_title: Mật khẩu + current_pass: + label: Mật khẩu hiện tại + msg: + empty: Mật khẩu hiện tại không thể trống. + length: Độ dài cần nằm trong khoảng từ 8 đến 32. + different: Hai mật khẩu nhập vào không khớp. + new_pass: + label: Mật khẩu mới + pass_confirm: + label: Xác nhận mật khẩu mới + interface: + heading: Giao diện + lang: + label: Ngôn ngữ giao diện + text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang. + my_logins: + title: Đăng nhập của tôi + label: Đăng nhập hoặc đăng ký trên trang này bằng các tài khoản này. + modal_title: Xóa đăng nhập + modal_content: Bạn có chắc chắn muốn xóa đăng nhập này khỏi tài khoản của bạn không? + modal_confirm_btn: Xóa + remove_success: Đã xóa thành công + toast: + update: cập nhật thành công + update_password: Mật khẩu đã được thay đổi thành công. + flag_success: Cảm ơn bạn đã đánh dấu. + forbidden_operate_self: Không được phép thao tác trên chính mình + review: Sửa đổi của bạn sẽ được hiển thị sau khi được xem xét. + sent_success: Đã gửi thành công + related_question: + title: Related + answers: câu trả lời + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: Mời mọi người + desc: Mời những người bạn nghĩ có thể trả lời. + invite: Mời trả lời + add: Thêm người + search: Tìm kiếm người + question_detail: + action: Hành động + Asked: Đã hỏi + asked: đã hỏi + update: Đã chỉnh sửa + edit: đã chỉnh sửa + commented: đã bình luận + Views: Lượt xem + Follow: Theo dõi + Following: Đang theo dõi + follow_tip: Theo dõi câu hỏi này để nhận thông báo + answered: đã trả lời + closed_in: Đóng trong + show_exist: Hiển thị câu hỏi hiện tại. + useful: Hữu ích + question_useful: Nó hữu ích và rõ ràng + question_un_useful: Nó không rõ ràng hoặc không hữu ích + question_bookmark: Đánh dấu câu hỏi này + answer_useful: Nó hữu ích + answer_un_useful: Nó không hữu ích + answers: + title: Các câu trả lời + score: Điểm + newest: Mới nhất + oldest: Cũ nhất + btn_accept: Chấp nhận + btn_accepted: Đã chấp nhận + write_answer: + title: Câu trả lời của bạn + edit_answer: Chỉnh sửa câu trả lời hiện tại của tôi + btn_name: Đăng câu trả lời của bạn + add_another_answer: Thêm câu trả lời khác + confirm_title: Tiếp tục trả lời + continue: Tiếp tục + confirm_info: >- +

Bạn có chắc chắn muốn thêm một câu trả lời khác không?

Bạn có thể sử dụng liên kết chỉnh sửa để tinh chỉnh và cải thiện câu trả lời hiện tại của mình, thay vì.

+ empty: Câu trả lời không thể trống. + characters: nội dung phải có ít nhất 6 ký tự. + tips: + header_1: Cảm ơn câu trả lời của bạn + li1_1: Vui lòng chắc chắn trả lời câu hỏi. Cung cấp chi tiết và chia sẻ nghiên cứu của bạn. + li1_2: Hỗ trợ bất kỳ tuyên bố nào bạn đưa ra với tài liệu tham khảo hoặc kinh nghiệm cá nhân. + header_2: Nhưng tránh ... + li2_1: Yêu cầu trợ giúp, yêu cầu làm rõ, hoặc trả lời cho các câu trả lời khác. + reopen: + confirm_btn: Mở lại + title: Mở lại bài đăng này + content: Bạn có chắc chắn muốn mở lại không? + list: + confirm_btn: Danh sách + title: Danh sách bài đăng này + content: Bạn có chắc chắn muốn liệt kê không? + unlist: + confirm_btn: Gỡ bỏ khỏi danh sách + title: Gỡ bỏ bài đăng này khỏi danh sách + content: Bạn có chắc chắn muốn gỡ bỏ không? + pin: + title: Ghim bài đăng này + content: Bạn có chắc chắn muốn ghim toàn cầu không? Bài đăng này sẽ xuất hiện ở đầu tất cả các danh sách bài đăng. + confirm_btn: Ghim + delete: + title: Xóa bài đăng này + question: >- + Chúng tôi không khuyến khích xóa câu hỏi có câu trả lời vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai.

Việc xóa liên tục các câu hỏi đã được trả lời có thể dẫn đến việc tài khoản của bạn bị chặn không được phép hỏi. Bạn có chắc chắn muốn xóa không? + answer_accepted: >- +

Chúng tôi không khuyến khích xóa câu trả lời đã được chấp nhận vì làm như vậy sẽ tước đoạt kiến thức của độc giả trong tương lai.

Việc xóa liên tục các câu trả lời đã được chấp nhận có thể dẫn đến việc tài khoản của bạn bị chặn không được phép trả lời. Bạn có chắc chắn muốn xóa không? + other: Bạn có chắc chắn muốn xóa không? + tip_answer_deleted: Câu trả lời này đã bị xóa + undelete_title: Khôi phục bài đăng này + undelete_desc: Bạn có chắc chắn muốn khôi phục không? + btns: + confirm: Xác nhận + cancel: Hủy + edit: Chỉnh sửa + save: Lưu + delete: Xóa + undelete: Khôi phục + list: Danh sách + unlist: Gỡ bỏ khỏi danh sách + unlisted: Không được liệt kê + login: Đăng nhập + signup: Đăng ký + logout: Đăng xuất + verify: Xác minh + create: Create + approve: Phê duyệt + reject: Từ chối + skip: Bỏ qua + discard_draft: Hủy bản nháp + pinned: Đã ghim + all: Tất cả + question: Câu hỏi + answer: Câu trả lời + comment: Bình luận + refresh: Làm mới + resend: Gửi lại + deactivate: Ngừng kích hoạt + active: Hoạt động + suspend: Tạm ngừng + unsuspend: Bỏ vô hiệu hóa + close: Đóng + reopen: Mở lại + ok: Đồng ý + light: Phông nền sáng + dark: Tối + system_setting: Cài đặt hệ thống + default: Mặc định + reset: Đặt lại + tag: Thẻ + post_lowercase: bài đăng + filter: Lọc + ignore: Bỏ qua + submit: Gửi + normal: Bình thường + closed: Đã đóng + deleted: Đã xóa + deleted_permanently: Deleted permanently + pending: Đang chờ xử lý + more: Thêm + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: Kết quả tìm kiếm + keywords: Từ khóa + options: Tùy chọn + follow: Theo dõi + following: Đang theo dõi + counts: "{{count}} Kết quả" + counts_loading: "... Results" + more: Thêm + sort_btns: + relevance: Liên quan + newest: Mới nhất + active: Hoạt động + score: Điểm + more: Thêm + tips: + title: Mẹo tìm kiếm nâng cao + tag: "<1>[tag] tìm kiếm trong một thẻ" + user: "<1>user:username tìm kiếm theo tác giả" + answer: "<1>answers:0 câu hỏi chưa có câu trả lời" + score: "<1>score:3 bài đăng có điểm 3+" + question: "<1>is:question tìm kiếm câu hỏi" + is_answer: "<1>is:answer tìm kiếm câu trả lời" + empty: Chúng tôi không thể tìm thấy bất cứ thứ gì.
Thử các từ khóa khác hoặc ít cụ thể hơn. + share: + name: Chia sẻ + copy: Sao chép liên kết + via: Chia sẻ bài đăng qua... + copied: Đã sao chép + facebook: Chia sẻ lên Facebook + twitter: Share to X + cannot_vote_for_self: Bạn không thể bỏ phiếu cho bài đăng của chính mình. + modal_confirm: + title: Lỗi... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: Tài khoản mới của bạn đã được xác nhận; bạn sẽ được chuyển hướng đến trang chủ. + link: Tiếp tục đến trang chủ + oops: Rất tiếc! + invalid: Liên kết bạn đã dùng không còn hoạt động nữa. + confirm_new_email: Email của bạn đã được cập nhật. + confirm_new_email_invalid: >- + Xin lỗi, liên kết xác nhận này không còn hợp lệ. Có thể email của bạn đã được thay đổi? + unsubscribe: + page_title: Hủy đăng ký + success_title: Hủy đăng ký thành công + success_desc: Bạn đã được gỡ bỏ khỏi danh sách người đăng ký này và sẽ không nhận được thêm email từ chúng tôi. + link: Thay đổi cài đặt + question: + following_tags: Thẻ đang theo dõi + edit: Chỉnh sửa + save: Lưu + follow_tag_tip: Theo dõi các thẻ để tùy chỉnh danh sách câu hỏi của bạn. + hot_questions: Câu hỏi nổi bật + all_questions: Tất cả câu hỏi + x_questions: "{{ count }} Câu hỏi" + x_answers: "{{ count }} câu trả lời" + x_posts: "{{ count }} Posts" + questions: Câu hỏi + answers: Câu trả lời + newest: Mới nhất + active: Hoạt động + hot: Được nhiều quan tâm + frequent: Thường xuyên + recommend: Đề xuất + score: Điểm + unanswered: Chưa được trả lời + modified: đã chỉnh sửa + answered: đã trả lời + asked: đã hỏi + closed: đã đóng + follow_a_tag: Theo dõi một thẻ + more: Thêm + personal: + overview: Tổng quan + answers: Câu trả lời + answer: câu trả lời + questions: Câu hỏi + question: câu hỏi + bookmarks: Đánh dấu + reputation: Danh tiếng + comments: Bình luận + votes: Bình chọn + badges: Danh hiệu + newest: Mới nhất + score: Điểm + edit_profile: Chỉnh sửa hồ sơ + visited_x_days: "Đã truy cập {{ count }} ngày" + viewed: Đã xem + joined: Tham gia + comma: "," + last_login: Đã xem + about_me: Về tôi + about_me_empty: "// Xin chào, Thế giới !" + top_answers: Câu trả lời hàng đầu + top_questions: Câu hỏi hàng đầu + stats: Thống kê + list_empty: Không tìm thấy bài đăng.
Có thể bạn muốn chọn một thẻ khác? + content_empty: Không tìm thấy bài viết nào. + accepted: Đã chấp nhận + answered: đã trả lời + asked: đã hỏi + downvoted: đã bỏ phiếu xuống + mod_short: MOD + mod_long: Người điều hành + x_reputation: danh tiếng + x_votes: phiếu bầu nhận được + x_answers: câu trả lời + x_questions: câu hỏi + recent_badges: Huy hiệu gần đây + install: + title: Cài đặt + next: Tiếp theo + done: Hoàn thành + config_yaml_error: Không thể tạo file config.yaml. + lang: + label: Vui lòng chọn một ngôn ngữ + db_type: + label: Hệ quản trị cơ sở dữ liệu + db_username: + label: Tên người dùng + placeholder: root + msg: Tên người dùng không thể trống. + db_password: + label: Mật khẩu + placeholder: root + msg: Mật khẩu không thể trống. + db_host: + label: Máy chủ cơ sở dữ liệu + placeholder: "db:3306" + msg: Máy chủ cơ sở dữ liệu không thể trống. + db_name: + label: Tên cơ sở dữ liệu + placeholder: câu trả lời + msg: Tên cơ sở dữ liệu không thể trống. + db_file: + label: Tệp tin Database + placeholder: /data/answer.db + msg: Tệp cơ sở dữ liệu không thể trống. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: Tạo config.yaml + label: Tệp config.yaml đã được tạo. + desc: >- + Bạn có thể tạo tệp <1>config.yaml thủ công trong thư mục <1>/var/wwww/xxx/ và dán văn bản sau vào đó. + info: Sau khi bạn đã làm xong, nhấp vào nút "Tiếp theo". + site_information: Thông tin trang + admin_account: Tài khoản quản trị + site_name: + label: Tên trang + msg: Tên trang không thể trống. + msg_max_length: Tên trang phải có tối đa 30 ký tự. + site_url: + label: URL trang + text: Địa chỉ của trang của bạn. + msg: + empty: URL trang không thể trống. + incorrect: Định dạng URL trang không chính xác. + max_length: URL trang phải có tối đa 512 ký tự. + contact_email: + label: Email liên hệ + text: Địa chỉ email của người liên hệ chính phụ trách trang này. + msg: + empty: Email liên hệ không thể trống. + incorrect: Định dạng email liên hệ không chính xác. + login_required: + label: Riêng tư + switch: Yêu cầu đăng nhập + text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này. + admin_name: + label: Tên + msg: Tên không thể trống. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: Mật khẩu + text: >- + Bạn sẽ cần mật khẩu này để đăng nhập. Vui lòng lưu trữ nó ở một nơi an toàn. + msg: Mật khẩu không thể trống. + msg_min_length: Mật khẩu phải có ít nhất 8 ký tự. + msg_max_length: Mật khẩu phải có tối đa 32 ký tự. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: Email + text: Bạn sẽ cần email này để đăng nhập. + msg: + empty: Email không thể trống. + incorrect: Định dạng email không chính xác. + ready_title: Trang web của bạn đã sẵn sàng + ready_desc: >- + Nếu bạn cảm thấy muốn thay đổi thêm cài đặt nào đó, hãy truy cập <1>mục quản trị; tìm nó trong menu trang. + good_luck: "Chúc bạn vui vẻ và may mắn!" + warn_title: Cảnh báo + warn_desc: >- + Tệp <1>config.yaml đã tồn tại. Nếu bạn cần đặt lại bất kỳ mục cấu hình nào trong tệp này, vui lòng xóa nó trước. + install_now: Bạn có thể thử <1>cài đặt ngay bây giờ. + installed: Đã cài đặt + installed_desc: >- + Có vẻ như bạn đã cài đặt rồi. Để cài đặt lại, vui lòng xóa các bảng cơ sở dữ liệu cũ trước. + db_failed: Kết nối cơ sở dữ liệu thất bại + db_failed_desc: >- + Điều này có thể có nghĩa là thông tin cơ sở dữ liệu trong tệp <1>config.yaml của bạn không chính xác hoặc không thể thiết lập liên lạc với máy chủ cơ sở dữ liệu. Điều này có thể có nghĩa là máy chủ cơ sở dữ liệu của máy chủ của bạn đang bị tắt. + counts: + views: lượt xem + votes: bình chọn + answers: câu trả lời + accepted: Đã chấp nhận + page_error: + http_error: Lỗi HTTP {{ code }} + desc_403: Bạn không có quyền truy cập trang này. + desc_404: Thật không may, trang này không tồn tại. + desc_50X: Máy chủ đã gặp sự cố và không thể hoàn thành yêu cầu của bạn. + back_home: Quay lại trang chủ + page_maintenance: + desc: "Chúng tôi đang bảo trì, chúng tôi sẽ trở lại sớm." + nav_menus: + dashboard: Bảng điều khiển + contents: Nội dung + questions: Câu hỏi + answers: Câu trả lời + users: Người dùng + badges: Huy hiệu + flags: Cờ + settings: Cài đặt + general: Chung + interface: Giao diện + smtp: SMTP + branding: Thương hiệu + legal: Pháp lý + write: Viết + tos: Điều khoản dịch vụ + privacy: Quyền riêng tư + seo: SEO + customize: Tùy chỉnh + themes: Chủ đề + login: Đăng nhập + privileges: Đặc quyền + plugins: Plugins + installed_plugins: Plugin đã cài đặt + apperance: Appearance + website_welcome: Chào mừng bạn đến với {{site_name}} + user_center: + login: Đăng nhập + qrcode_login_tip: Vui lòng sử dụng {{ agentName }} để quét mã QR và đăng nhập. + login_failed_email_tip: Đăng nhập thất bại, vui lòng cho phép ứng dụng này truy cập thông tin email của bạn trước khi thử lại. + badges: + modal: + title: Chúc mừng + content: Bạn đã nhận được huy hiệu mới. + close: Đóng + confirm: Xem huy hiệu + title: Huy hiệu + awarded: Giải Thưởng + earned_×: Nhận được ×{{ number }} + ×_awarded: "{{ number }} được trao tặng" + can_earn_multiple: Bạn có thể kiếm được nhiều lần. + earned: Đã nhận + admin: + admin_header: + title: Quản trị + dashboard: + title: Bảng điều khiển + welcome: Chào mừng bạn đến với Answer Admin! + site_statistics: Thống kê trang + questions: "Câu hỏi:" + resolved: "Đã giải quyết:" + unanswered: "Chưa được trả lời:" + answers: "Câu trả lời:" + comments: "Bình luận:" + votes: "Phiếu bầu:" + users: "Người dùng:" + flags: "Cờ:" + reviews: "Đánh giá:" + site_health: Sức khỏe trang + version: "Phiên bản:" + https: "HTTPS:" + upload_folder: "Thư mục tải lên:" + run_mode: "Chế độ hoạt động:" + private: Riêng tư + public: Công cộng + smtp: "SMTP:" + timezone: "Múi giờ:" + system_info: Thông tin hệ thống + go_version: "Phiên bản Go:" + database: "Database:" + database_size: "Tệp tin Database:" + storage_used: "Bộ nhớ đã sử dụng:" + uptime: "Thời gian hoạt động:" + links: Links + plugins: Plugin + github: GitHub + blog: Blog + contact: Liên hệ + forum: Diễn đàn + documents: Tài liệu + feedback: Phản hồi + support: Hỗ trợ + review: Đánh giá + config: Cấu hình + update_to: Cập nhật lên + latest: Mới nhất + check_failed: Kiểm tra thất bại + "yes": "Có" + "no": "Không" + not_allowed: Không được phép + allowed: Được phép + enabled: Đã bật + disabled: Đã tắt + writable: Có thể chỉnh sửa + not_writable: Không thể ghi + flags: + title: Cờ + pending: Đang chờ xử lý + completed: Hoàn thành + flagged: Đã đánh dấu + flagged_type: Đã đánh dấu {{ type }} + created: Đã tạo + action: Hành động + review: Đánh giá + user_role_modal: + title: Thay đổi vai trò người dùng thành... + btn_cancel: Hủy + btn_submit: Gửi + new_password_modal: + title: Đặt mật khẩu mới + form: + fields: + password: + label: Mật khẩu + text: Người dùng sẽ bị đăng xuất và cần đăng nhập lại. + msg: Mật khẩu phải có độ dài từ 8 đến 32 ký tự. + btn_cancel: Hủy + btn_submit: Gửi + edit_profile_modal: + title: Chỉnh sửa hồ sơ + form: + fields: + display_name: + label: Tên hiển thị + msg_range: Display name must be 2-30 characters in length. + username: + label: Tên người dùng + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Địa chỉ email không hợp lệ. + edit_success: Chỉnh Sửa Thành Công + btn_cancel: Hủy + btn_submit: Gửi + user_modal: + title: Thêm người dùng mới + form: + fields: + users: + label: Thêm người dùng hàng loạt + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Tách "tên, email, mật khẩu" bằng dấu phẩy. Một người dùng mỗi dòng. + msg: "Vui lòng nhập email của người dùng, một dòng mỗi người." + display_name: + label: Tên hiển thị + msg: Tên hiển thị phải dài từ 2-30 ký tự. + email: + label: Email + msg: Email không hợp lệ. + password: + label: Mật khẩu + msg: Mật khẩu phải có từ 8 đến 32 ký tự. + btn_cancel: Hủy + btn_submit: Gửi + users: + title: Người dùng + name: Tên + email: Email + reputation: Danh tiếng + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: Trạng thái + role: Vai trò + action: Hành động + change: Thay đổi + all: Tất cả + staff: Nhân viên + more: Thêm + inactive: Không hoạt động + suspended: Bị tạm ngưng + deleted: Đã xóa + normal: Bình thường + Moderator: Người điều hành + Admin: Quản trị viên + User: Người dùng + filter: + placeholder: "Lọc theo tên, user:id" + set_new_password: Đặt mật khẩu mới + edit_profile: Chỉnh sửa hồ sơ + change_status: Thay đổi trạng thái + change_role: Thay đổi vai trò + show_logs: Hiển thị nhật ký + add_user: Thêm người dùng + deactivate_user: + title: Ngừng kích hoạt người dùng + content: Người dùng không hoạt động phải xác nhận lại email của họ. + delete_user: + title: Xóa người dùng này + content: Bạn có chắc chắn muốn xóa người dùng này không? Điều này là vĩnh viễn! + remove: Xóa nội dung của họ + label: Xóa tất cả các câu hỏi, câu trả lời, bình luận, vv. + text: Không chọn điều này nếu bạn chỉ muốn xóa tài khoản của người dùng. + suspend_user: + title: Đình chỉ người dùng này + content: Người dùng bị đình chỉ không thể đăng nhập. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: Câu hỏi + unlisted: Không được liệt kê + post: Bài đăng + votes: Phiếu bầu + answers: Câu trả lời + created: Đã tạo + status: Trạng thái + action: Hành động + change: Thay đổi + pending: Đang chờ xử lý + filter: + placeholder: "Lọc theo tiêu đề, question:id" + answers: + page_title: Câu trả lời + post: Bài đăng + votes: Phiếu bầu + created: Đã tạo + status: Trạng thái + action: Hành động + change: Thay đổi + filter: + placeholder: "Lọc theo tiêu đề, answer:id" + general: + page_title: Chung + name: + label: Tên trang + msg: Tên trang không thể trống. + text: "Tên của trang này, được sử dụng trong thẻ tiêu đề." + site_url: + label: URL trang + msg: Url trang không thể trống. + validate: Vui lòng nhập URL hợp lệ. + text: Địa chỉ của trang của bạn. + short_desc: + label: Mô tả ngắn của trang + msg: Mô tả ngắn của trang không thể trống. + text: "Mô tả ngắn, được sử dụng trong thẻ tiêu đề trên trang chủ." + desc: + label: Mô tả trang + msg: Mô tả trang không thể trống. + text: "Mô tả trang này trong một câu, được sử dụng trong thẻ mô tả meta." + contact_email: + label: Email liên hệ + msg: Email liên hệ không thể trống. + validate: Định dạng email liên hệ không hợp lệ. + text: Địa chỉ email của người liên hệ chính phụ trách trang này. + check_update: + label: Cập nhật phần mềm + text: Tự động kiểm tra cập nhật + interface: + page_title: Giao diện + language: + label: Ngôn ngữ giao diện + msg: Ngôn ngữ giao diện không thể trống. + text: Ngôn ngữ giao diện người dùng. Nó sẽ thay đổi khi bạn làm mới trang. + time_zone: + label: Múi giờ + msg: Múi giờ không thể trống. + text: Chọn một thành phố cùng múi giờ với bạn. + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: Email gửi từ + msg: Email gửi từ không thể trống. + text: Địa chỉ email mà các email được gửi từ đó. + from_name: + label: Tên gửi từ + msg: Tên gửi từ không thể trống. + text: Tên mà các email được gửi từ đó. + smtp_host: + label: Máy chủ SMTP + msg: Máy chủ SMTP không thể trống. + text: Máy chủ thư của bạn. + encryption: + label: Mã hóa + msg: Mã hóa không thể trống. + text: Đối với hầu hết các máy chủ, SSL là tùy chọn được khuyến nghị. + ssl: SSL + tls: TLS + none: Không + smtp_port: + label: Cổng SMTP + msg: Cổng SMTP phải là số từ 1 đến 65535. + text: Cổng đến máy chủ thư của bạn. + smtp_username: + label: Tên người dùng SMTP + msg: Tên người dùng SMTP không thể trống. + smtp_password: + label: Mật khẩu SMTP + msg: Mật khẩu SMTP không thể trống. + test_email_recipient: + label: Người nhận email kiểm tra + text: Cung cấp địa chỉ email sẽ nhận email kiểm tra. + msg: Người nhận email kiểm tra không hợp lệ + smtp_authentication: + label: Bật xác thực + title: Xác thực SMTP + msg: Xác thực SMTP không thể trống. + "yes": "Có" + "no": "Không" + branding: + page_title: Thương hiệu + logo: + label: Logo + msg: Logo không thể trống. + text: Hình ảnh logo ở góc trên bên trái của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56 và tỷ lệ khung hình lớn hơn 3:1. Nếu để trống, văn bản tiêu đề trang sẽ được hiển thị. + mobile_logo: + label: Logo di động + text: Logo được sử dụng trên phiên bản di động của trang của bạn. Sử dụng hình ảnh hình chữ nhật rộng với chiều cao 56. Nếu để trống, hình ảnh từ cài đặt "logo" sẽ được sử dụng. + square_icon: + label: Biểu tượng vuông + msg: Biểu tượng vuông không thể trống. + text: Hình ảnh được sử dụng làm cơ sở cho các biểu tượng siêu dữ liệu. Nên lớn hơn 512x512. + favicon: + label: Favicon + text: Favicon cho trang của bạn. Để hoạt động chính xác trên một CDN, nó phải là png. Sẽ được thay đổi kích thước thành 32x32. Nếu để trống, "biểu tượng vuông" sẽ được sử dụng. + legal: + page_title: Pháp lý + terms_of_service: + label: Điều khoản dịch vụ + text: "Bạn có thể thêm nội dung điều khoản dịch vụ ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây." + privacy_policy: + label: Chính sách bảo mật + text: "Bạn có thể thêm nội dung chính sách bảo mật ở đây. Nếu bạn đã có một tài liệu được lưu trữ ở nơi khác, cung cấp URL đầy đủ ở đây." + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: Viết + restrict_answer: + title: Câu trả lời chỉnh sửa + label: Each user can only write one answer for each question + text: "Tắt để cho phép người dùng viết nhiều câu trả lời cho cùng một câu hỏi, điều này có thể khiến các câu trả lời bị mất trọng tâm." + recommend_tags: + label: Thẻ được đề xuất + text: "Các thẻ gợi ý sẽ hiển thị trong danh sách thả xuống theo mặc định." + msg: + contain_reserved: "các thẻ được đề xuất không được chứa thẻ dự bị" + required_tag: + title: Đặt thẻ cần thiết + label: Đặt thẻ được đề xuất là bắt buộc + text: "Mỗi câu hỏi mới phải có ít nhất một thẻ được đề xuất." + reserved_tags: + label: Thẻ dành riêng + text: "Thẻ dành riêng chỉ có thể được thêm vào một bài đăng bởi điều hành viên." + image_size: + label: Kích thước hình ảnh tối đa (MB) + text: "Kích thước tải lên hình ảnh tối đa." + attachment_size: + label: Kích thước tệp đính kèm tối đa (MB) + text: "Kích thước tải lên tệp đính kèm tối đa." + image_megapixels: + label: Megapixel hình ảnh tối đa + text: "Số megapixel tối đa được phép cho một hình ảnh." + image_extensions: + label: Tiện ích mở rộng hình ảnh được ủy quyền + text: "Danh sách đuôi file được phép hiển thị hình ảnh, phân cách bằng dấu phẩy." + attachment_extensions: + label: Các loại tệp đính kèm được phép tải lên + text: "Danh sách các đuôi file được phép tải lên, phân cách bằng dấu phẩy. CẢNH BÁO: Cho phép tải lên có thể gây ra vấn đề bảo mật." + seo: + page_title: SEO + permalink: + label: Liên kết cố định + text: Cấu trúc URL tùy chỉnh có thể cải thiện khả năng sử dụng và khả năng tương thích về sau của liên kết của bạn. + robots: + label: robots.txt + text: Điều này sẽ ghi đè vĩnh viễn bất kỳ cài đặt trang web liên quan nào. + themes: + page_title: Giao diện + themes: + label: Giao diện + text: Chọn một chủ đề hiện có. + color_scheme: + label: Sơ đồ màu + navbar_style: + label: Navbar background style + primary_color: + label: Màu chính + text: Thay đổi các màu sắc được sử dụng bởi chủ đề của bạn + css_and_html: + page_title: CSS và HTML + custom_css: + label: CSS tùy chỉnh + text: > + + head: + label: Đầu + text: > + + header: + label: Đầu trang + text: > + + footer: + label: Cuối trang + text: Điều này sẽ chèn trước </body>. + sidebar: + label: Thanh bên + text: Điều này sẽ chèn vào thanh bên. + login: + page_title: Đăng nhập + membership: + title: Thành viên + label: Cho phép đăng ký mới + text: Tắt để ngăn ai đó tạo tài khoản mới. + email_registration: + title: Đăng ký qua email + label: Cho phép đăng ký qua email + text: Tắt để ngăn ai đó tạo tài khoản mới thông qua email. + allowed_email_domains: + title: Miền email được phép + text: Miền email mà người dùng phải đăng ký tài khoản. Một miền mỗi dòng. Bỏ qua khi trống. + private: + title: Riêng tư + label: Yêu cầu đăng nhập + text: Chỉ người dùng đã đăng nhập mới có thể truy cập cộng đồng này. + password_login: + title: Đăng nhập bằng mật khẩu + label: Cho phép đăng nhập bằng email và mật khẩu + text: "CẢNH BÁO: Nếu tắt, bạn có thể không thể đăng nhập nếu bạn chưa cấu hình phương thức đăng nhập khác trước đó." + installed_plugins: + title: Plugin đã cài đặt + plugin_link: Plugin mở rộng và mở rộng chức năng của trang web. Bạn có thể tìm thấy plugin trong <1>Kho Plugin Answer. + filter: + all: Tất cả + active: Đang hoạt động + inactive: Không hoạt động + outdated: Quá hạn + plugins: + label: Plugin + text: Chọn một plugin hiện có. + name: Tên + version: Phiên bản + status: Trạng thái + action: Hành động + deactivate: Vô hiệu hóa + activate: Kích hoạt + settings: Cài đặt + settings_users: + title: Người dùng + avatar: + label: Hình đại diện mặc định + text: Dành cho người dùng không có hình đại diện tùy chỉnh của riêng họ. + gravatar_base_url: + label: Gravatar Base URL + text: URL của nhà cung cấp API Gravatar. Bỏ qua khi trống. + profile_editable: + title: Hồ sơ có thể chỉnh sửa + allow_update_display_name: + label: Cho phép người dùng thay đổi tên hiển thị của họ + allow_update_username: + label: Cho phép người dùng thay đổi tên người dùng của họ + allow_update_avatar: + label: Cho phép người dùng thay đổi hình ảnh hồ sơ của họ + allow_update_bio: + label: Cho phép người dùng thay đổi giới thiệu về mình + allow_update_website: + label: Cho phép người dùng thay đổi trang web của họ + allow_update_location: + label: Cho phép người dùng thay đổi vị trí của họ + privilege: + title: Đặc quyền + level: + label: Mức độ danh tiếng yêu cầu + text: Chọn mức danh tiếng yêu cầu cho các đặc quyền + msg: + should_be_number: dữ liệu đầu vào phải là kiểu số + number_larger_1: số phải bằng hoặc lớn hơn 1 + badges: + action: Hành động + active: Hoạt động + activate: Kích hoạt + all: Tất cả + awards: Giải Thưởng + deactivate: Ngừng kích hoạt + filter: + placeholder: Lọc theo tên, user:id + group: Nhóm + inactive: Không hoạt động + name: Tên + show_logs: Hiển thị nhật ký + status: Trạng thái + title: Danh hiệu + form: + optional: (tùy chọn) + empty: không thể trống + invalid: không hợp lệ + btn_submit: Lưu + not_found_props: "Không tìm thấy thuộc tính bắt buộc {{ key }}." + select: Chọn + page_review: + review: Xem xét + proposed: đề xuất + question_edit: Chỉnh sửa câu hỏi + answer_edit: Câu trả lời chỉnh sửa + tag_edit: Chỉnh sửa thẻ + edit_summary: Tóm tắt chỉnh sửa + edit_question: Chỉnh sửa câu hỏi + edit_answer: Chỉnh sửa câu trả lời + edit_tag: Chỉnh sửa thẻ + empty: Không còn nhiệm vụ xem xét nào. + approve_revision_tip: Bạn có chấp nhận sửa đổi này không? + approve_flag_tip: Bạn có chấp nhận cờ này không? + approve_post_tip: Bạn có chấp nhận bài đăng này không? + approve_user_tip: Bạn có chấp nhận người dùng này không? + suggest_edits: Đề xuất chỉnh sửa + flag_post: Đánh dấu bài đăng + flag_user: Đánh dấu người dùng + queued_post: Bài đăng trong hàng đợi + queued_user: Người dùng trong hàng đợi + filter_label: Loại + reputation: danh tiếng + flag_post_type: Đánh dấu bài đăng này là {{ type }}. + flag_user_type: Đánh dấu người dùng này là {{ type }}. + edit_post: Chỉnh sửa bài đăng + list_post: Liệt kê bài đăng + unlist_post: Gỡ bỏ bài đăng khỏi danh sách + timeline: + undeleted: đã khôi phục + deleted: đã xóa + downvote: bỏ phiếu xuống + upvote: bỏ phiếu lên + accept: chấp nhận + cancelled: đã hủy + commented: đã bình luận + rollback: quay lại + edited: đã chỉnh sửa + answered: đã trả lời + asked: đã hỏi + closed: đã đóng + reopened: đã mở lại + created: đã tạo + pin: đã ghim + unpin: bỏ ghim + show: được liệt kê + hide: không được liệt kê + title: "Lịch sử cho" + tag_title: "Dòng thời gian cho" + show_votes: "Hiển thị phiếu bầu" + n_or_a: N/A + title_for_question: "Dòng thời gian cho" + title_for_answer: "Dòng thời gian cho câu trả lời của {{ title }} bởi {{ author }}" + title_for_tag: "Dòng thời gian cho thẻ" + datetime: Ngày giờ + type: Loại + by: Bởi + comment: Bình luận + no_data: "Chúng tôi không thể tìm thấy bất cứ thứ gì." + users: + title: Người dùng + users_with_the_most_reputation: Người dùng có điểm danh tiếng cao nhất trong tuần này + users_with_the_most_vote: Người dùng đã bỏ phiếu nhiều nhất trong tuần này + staffs: Nhân viên cộng đồng của chúng tôi + reputation: danh tiếng + votes: phiếu bầu + prompt: + leave_page: Bạn có chắc chắn muốn rời khỏi trang không? + changes_not_save: Các thay đổi của bạn có thể không được lưu. + draft: + discard_confirm: Bạn có chắc chắn muốn hủy bản nháp của mình không? + messages: + post_deleted: Bài đăng này đã bị xóa. + post_cancel_deleted: Bài đăng này đã được phục hồi. + post_pin: Bài đăng này đã được ghim. + post_unpin: Bài đăng này đã bị bỏ ghim. + post_hide_list: Bài đăng này đã được ẩn khỏi danh sách. + post_show_list: Bài đăng này đã được hiển thị trên danh sách. + post_reopen: Bài đăng này đã được mở lại. + post_list: Bài đăng này đã được liệt kê. + post_unlist: Bài đăng này đã được gỡ bỏ khỏi danh sách. + post_pending: Bài đăng của bạn đang chờ xem xét. Đây là bản xem trước, nó sẽ được hiển thị sau khi được phê duyệt. + post_closed: Bài đăng này đã bị đóng. + answer_deleted: Câu trả lời này đã bị xóa. + answer_cancel_deleted: Câu trả lời này đã được phục hồi. + change_user_role: Vai trò của người dùng này đã được thay đổi. + user_inactive: Người dùng này đã không hoạt động. + user_normal: Người dùng này đã bình thường. + user_suspended: Người dùng này đã bị đình chỉ. + user_deleted: Người dùng này đã bị xóa. + badge_activated: Huy hiệu này đã được kích hoạt. + badge_inactivated: Huy hiệu này đã bị vô hiệu hóa. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 15c951268..b62fd4700 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,170 +1,2341 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" - email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" - report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: +# The following fields are used for back-end +backend: + base: + success: + other: 成功。 + unknown: + other: 未知错误。 + request_format_error: + other: 请求格式错误。 + unauthorized_error: + other: 未授权。 + database_error: + other: 数据服务器错误。 + forbidden_error: + other: 禁止访问。 + duplicate_request_error: + other: 重复提交。 + action: + report: + other: 举报 + edit: + other: 编辑 + delete: + other: 删除 + close: + other: 关闭 + reopen: + other: 重新打开 + forbidden_error: + other: 禁止访问。 + pin: + other: 置顶 + hide: + other: 列表隐藏 + unpin: + other: 取消置顶 + show: + other: 列表显示 + invite_someone_to_answer: + other: 编辑 + undelete: + other: 撤消删除 + merge: + other: 合并 + role: name: - other: "其他原因" + user: + other: 用户 + admin: + other: 管理员 + moderator: + other: 版主 description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" - -question: - close: - duplicate: - name: - other: "垃圾信息" + user: + other: 默认没有特殊权限。 + admin: + other: 拥有管理网站的全部权限。 + moderator: + other: 拥有除访问后台管理以外的所有权限。 + privilege: + level_1: description: - other: "此问题以前就有人问过,而且已经有了答案。" - guideline: - name: - other: "社区特定原因" + other: 级别 1(少量声望要求,适合私有团队、群组) + level_2: description: - other: "此问题不符合社区准则。" - multiple: - name: - other: "需要细节或澄清" + other: 级别 2(低声望要求,适合初启动的社区) + level_3: description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" - other: - name: - other: "其他原因" + other: 级别 3(高声望要求,适合成熟的社区) + level_custom: description: - other: "此帖子需要上述所列以外的其他理由。" + other: 自定义等级 + rank_question_add_label: + other: 提问 + rank_answer_add_label: + other: 写答案 + rank_comment_add_label: + other: 写评论 + rank_report_add_label: + other: 举报 + rank_comment_vote_up_label: + other: 点赞评论 + rank_link_url_limit_label: + other: 每次发布超过 2 个链接 + rank_question_vote_up_label: + other: 点赞问题 + rank_answer_vote_up_label: + other: 点赞答案 + rank_question_vote_down_label: + other: 点踩问题 + rank_answer_vote_down_label: + other: 点踩答案 + rank_invite_someone_to_answer_label: + other: 邀请回答 + rank_tag_add_label: + other: 创建新标签 + rank_tag_edit_label: + other: 编辑标签描述(需要审核) + rank_question_edit_label: + other: 编辑别人的问题(需要审核) + rank_answer_edit_label: + other: 编辑别人的答案(需要审核) + rank_question_edit_without_review_label: + other: 编辑别人的问题无需审核 + rank_answer_edit_without_review_label: + other: 编辑别人的答案无需审核 + rank_question_audit_label: + other: 审核问题编辑 + rank_answer_audit_label: + other: 审核回答编辑 + rank_tag_audit_label: + other: 审核标签编辑 + rank_tag_edit_without_review_label: + other: 编辑标签描述无需审核 + rank_tag_synonym_label: + other: 管理标签同义词 + email: + other: 邮箱 + e_mail: + other: 邮箱 + password: + other: 密码 + pass: + other: 密码 + old_pass: + other: 当前密码 + original_text: + other: 本帖 + email_or_password_wrong_error: + other: 邮箱和密码不匹配。 + error: + common: + invalid_url: + other: 无效的 URL。 + status_invalid: + other: 无效状态。 + password: + space_invalid: + other: 密码不得含有空格。 + admin: + cannot_update_their_password: + other: 你无法修改自己的密码。 + cannot_edit_their_profile: + other: 您不能修改您的个人资料。 + cannot_modify_self_status: + other: 你无法修改自己的状态。 + email_or_password_wrong: + other: 邮箱和密码不匹配。 + answer: + not_found: + other: 没有找到答案。 + cannot_deleted: + other: 没有删除权限。 + cannot_update: + other: 没有更新权限。 + question_closed_cannot_add: + other: 问题已关闭,无法添加。 + content_cannot_empty: + other: 回答内容不能为空。 + comment: + edit_without_permission: + other: 不允许编辑评论。 + not_found: + other: 评论未找到。 + cannot_edit_after_deadline: + other: 评论时间太久,无法修改。 + content_cannot_empty: + other: 评论内容不能为空。 + email: + duplicate: + other: 邮箱已存在。 + need_to_be_verified: + other: 邮箱需要验证。 + verify_url_expired: + other: 邮箱验证的网址已过期,请重新发送邮件。 + illegal_email_domain_error: + other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 + lang: + not_found: + other: 语言文件未找到。 + object: + captcha_verification_failed: + other: 验证码错误。 + disallow_follow: + other: 你不能关注。 + disallow_vote: + other: 你不能投票。 + disallow_vote_your_self: + other: 你不能为自己的帖子投票。 + not_found: + other: 对象未找到。 + verification_failed: + other: 验证失败。 + email_or_password_incorrect: + other: 邮箱和密码不匹配。 + old_password_verification_failed: + other: 旧密码验证失败。 + new_password_same_as_previous_setting: + other: 新密码和旧密码相同。 + already_deleted: + other: 该帖子已被删除。 + meta: + object_not_found: + other: Meta 对象未找到 + question: + already_deleted: + other: 该帖子已被删除。 + under_review: + other: 您的帖子正在等待审核。它将在它获得批准后可见。 + not_found: + other: 问题未找到。 + cannot_deleted: + other: 没有删除权限。 + cannot_close: + other: 没有关闭权限。 + cannot_update: + other: 没有更新权限。 + content_cannot_empty: + other: 内容不能为空。 + rank: + fail_to_meet_the_condition: + other: 声望值未达到要求。 + vote_fail_to_meet_the_condition: + other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 + no_enough_rank_to_operate: + other: 你至少需要 {{.Rank}} 声望才能执行此操作。 + report: + handle_failed: + other: 报告处理失败。 + not_found: + other: 报告未找到。 + tag: + already_exist: + other: 标签已存在。 + not_found: + other: 标签未找到。 + recommend_tag_not_found: + other: 推荐标签不存在。 + recommend_tag_enter: + other: 请选择至少一个必选标签。 + not_contain_synonym_tags: + other: 不应包含同义词标签。 + cannot_update: + other: 没有更新权限。 + is_used_cannot_delete: + other: 你不能删除这个正在使用的标签。 + cannot_set_synonym_as_itself: + other: 你不能将当前标签设为自己的同义词。 + smtp: + config_from_name_cannot_be_email: + other: 发件人名称不能是邮箱地址。 + theme: + not_found: + other: 主题未找到。 + revision: + review_underway: + other: 目前无法编辑,有一个版本在审阅队列中。 + no_permission: + other: 无权限修改。 + user: + external_login_missing_user_id: + other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 + external_login_unbinding_forbidden: + other: 请在移除此登录之前为你的账户设置登录密码。 + email_or_password_wrong: + other: + other: 邮箱和密码不匹配。 + not_found: + other: 用户未找到。 + suspended: + other: 用户已被封禁。 + username_invalid: + other: 用户名无效。 + username_duplicate: + other: 用户名已被使用。 + set_avatar: + other: 头像设置错误。 + cannot_update_your_role: + other: 你不能修改自己的角色。 + not_allowed_registration: + other: 该网站暂未开放注册。 + not_allowed_login_via_password: + other: 该网站暂不支持密码登录。 + access_denied: + other: 拒绝访问 + page_access_denied: + other: 您没有权限访问此页面。 + add_bulk_users_format_error: + other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" + status_suspended_forever: + other: "该用户已被永久封禁。该用户不符合社区准则。" + status_suspended_until: + other: "该用户已被封禁至 {{.SuspendedUntil}}。该用户不符合社区准则。" + status_deleted: + other: "该用户已被删除。" + status_inactive: + other: "该用户未激活。" + config: + read_config_failed: + other: 读取配置失败 + database: + connection_failed: + other: 数据库连接失败 + create_table_failed: + other: 创建表失败 + install: + create_config_failed: + other: 无法创建 config.yaml 文件。 + upload: + unsupported_file_format: + other: 不支持的文件格式。 + site_info: + config_not_found: + other: 未找到网站的该配置信息。 + badge: + object_not_found: + other: 没有找到徽章对象 + reason: + spam: + name: + other: 垃圾信息 + desc: + other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 + rude_or_abusive: + name: + other: 粗鲁或辱骂的 + desc: + other: "一个有理智的人都会认为这种内容不适合进行尊重性的讨论。" + a_duplicate: + name: + other: 重复内容 + desc: + other: 该问题有人问过,而且已经有了答案。 + placeholder: + other: 输入已有的问题链接 + not_a_answer: + name: + other: 不是答案 + desc: + other: "该帖是作为答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑、评论、另一个问题或者需要被删除。" + no_longer_needed: + name: + other: 不再需要 + desc: + other: 该评论已过时,对话性质或与此帖子无关。 + something: + name: + other: 其他原因 + desc: + other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 + placeholder: + other: 让我们具体知道你关心的什么 + community_specific: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + not_clarity: + name: + other: 需要细节或澄清 + desc: + other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 + looks_ok: + name: + other: 看起来没问题 + desc: + other: 这个帖子是好的,不是低质量。 + needs_edit: + name: + other: 需要编辑,我已做了修改。 + desc: + other: 改进和纠正你自己帖子中的问题。 + needs_close: + name: + other: 需要关闭 + desc: + other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 + needs_delete: + name: + other: 需要删除 + desc: + other: 该帖子将被删除。 + question: + close: + duplicate: + name: + other: 垃圾信息 + desc: + other: 此问题以前就有人问过,而且已经有了答案。 + guideline: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + multiple: + name: + other: 需要细节或澄清 + desc: + other: 该问题目前涵盖多个问题。它应该只集中在一个问题上。 + other: + name: + other: 其他原因 + desc: + other: 该帖子存在上面没有列出的另一个原因。 + operation_type: + asked: + other: 提问于 + answered: + other: 回答于 + modified: + other: 修改于 + deleted_title: + other: 删除的问题 + questions_title: + other: 问题 + tag: + tags_title: + other: 标签 + no_description: + other: 此标签没有描述。 + notification: + action: + update_question: + other: 更新了问题 + answer_the_question: + other: 回答了问题 + update_answer: + other: 更新了答案 + accept_answer: + other: 采纳了答案 + comment_question: + other: 评论了问题 + comment_answer: + other: 评论了答案 + reply_to_you: + other: 回复了你 + mention_you: + other: 提到了你 + your_question_is_closed: + other: 你的问题已被关闭 + your_question_was_deleted: + other: 你的问题已被删除 + your_answer_was_deleted: + other: 你的答案已被删除 + your_comment_was_deleted: + other: 你的评论已被删除 + up_voted_question: + other: 点赞问题 + down_voted_question: + other: 点踩问题 + up_voted_answer: + other: 点赞答案 + down_voted_answer: + other: 点踩回答 + up_voted_comment: + other: 点赞评论 + invited_you_to_answer: + other: 邀请你回答 + earned_badge: + other: 你获得 "{{.BadgeName}}" 徽章 + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] 确认你的新邮箱地址" + body: + other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

" + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" + new_question: + title: + other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}

\n{{.Tags}}

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到

\n\n取消订阅" + pass_reset: + title: + other: "[{{.SiteName }}] 重置密码" + body: + other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n\n如果你没有请求此更改,请忽略此邮件。\n" + register: + title: + other: "[{{.SiteName}}] 确认你的新账户" + body: + other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n

\n\n--
\n这是系统自动发送的电子邮件,请勿回复,因为您的回复将不会被看到" + test: + title: + other: "[{{.SiteName}}] 测试邮件" + body: + other: "这是测试电子邮件。\n

\n\n-
\n注意:这是一个自动的系统电子邮件, 请不要回复此消息,因为您的回复将不会被看到。" + action_activity_type: + upvote: + other: 点赞 + upvoted: + other: 点赞 + downvote: + other: 点踩 + downvoted: + other: 点踩 + accept: + other: 采纳 + accepted: + other: 已采纳 + edit: + other: 编辑 + review: + queued_post: + other: 排队的帖子 + flagged_post: + other: 举报的帖子 + suggested_post_edit: + other: 建议的编辑 + reaction: + tooltip: + other: "{{ .Names }} 以及另外 {{ .Count }} 个..." + badge: + default_badges: + autobiographer: + name: + other: 自传作者 + desc: + other: 填写了 个人资料 信息。 + certified: + name: + other: 已认证 + desc: + other: 完成了我们的新用户教程。 + editor: + name: + other: 编辑者 + desc: + other: 首次帖子编辑。 + first_flag: + name: + other: 第一次举报 + desc: + other: 第一次举报一个帖子 + first_upvote: + name: + other: 第一次投票 + desc: + other: 第一次投票了一个帖子。 + first_link: + name: + other: 第一个链接 + desc: + other: 第一次添加了一个链接到另一个帖子。 + first_reaction: + name: + other: 第一个响应 + desc: + other: 第一次表情回应帖子 + first_share: + name: + other: 首次分享 + desc: + other: 首次分享了一个帖子。 + scholar: + name: + other: 学者 + desc: + other: 问了一个问题并接受了一个答案。 + commentator: + name: + other: 评论员 + desc: + other: 留下5条评论。 + new_user_of_the_month: + name: + other: 月度用户 + desc: + other: 本月杰出用户 + read_guidelines: + name: + other: 阅读指南 + desc: + other: 阅读[社区准则]。 + reader: + name: + other: 读者 + desc: + other: 用10个以上的答案在主题中阅读每个答案。 + welcome: + name: + other: 欢迎 + desc: + other: 获得一个点赞投票 + nice_share: + name: + other: 好分享 + desc: + other: 分享了一个拥有25个唯一访客的帖子。 + good_share: + name: + other: 好分享 + desc: + other: 分享了一个拥有300个唯一访客的帖子。 + great_share: + name: + other: 优秀的分享 + desc: + other: 分享了一个拥有1000个唯一访客的帖子。 + out_of_love: + name: + other: 失去爱好 + desc: + other: 一天内使用了 50 个赞。 + higher_love: + name: + other: 更高的爱好 + desc: + other: 一天内使用了 50 个赞 5 次。 + crazy_in_love: + name: + other: 爱情疯狂的 + desc: + other: 一天内使用了 50 个赞 20 次。 + promoter: + name: + other: 推荐人 + desc: + other: 邀请用户。 + campaigner: + name: + other: 宣传者 + desc: + other: 邀请了3个基本用户。 + champion: + name: + other: 冠军 + desc: + other: 邀请了5个成员。 + thank_you: + name: + other: 谢谢 + desc: + other: 有 20 个赞成票的帖子,并投了 10 个赞成票。 + gives_back: + name: + other: 返回 + desc: + other: 拥有100个投票赞成的职位并放弃了100个投票。 + empathetic: + name: + other: 情随境迁 + desc: + other: 拥有500个投票赞成的职位并放弃了1000个投票。 + enthusiast: + name: + other: 狂热 + desc: + other: 连续访问10天。 + aficionado: + name: + other: Aficionado + desc: + other: 连续访问100天。 + devotee: + name: + other: Devotee + desc: + other: 连续访问365天。 + anniversary: + name: + other: 周年纪念日 + desc: + other: 活跃成员一年至少发布一次。 + appreciated: + name: + other: 欣赏 + desc: + other: 在 20 个帖子中获得 1个投票 + respected: + name: + other: 尊敬 + desc: + other: 100个员额获得2次补票。 + admired: + name: + other: 仰慕 + desc: + other: 300个员额获得5次补票。 + solved: + name: + other: 已解决 + desc: + other: 接受答案。 + guidance_counsellor: + name: + other: 指导顾问 + desc: + other: 接受答案。 + know_it_all: + name: + other: 万事通 + desc: + other: 接受50个答案。 + solution_institution: + name: + other: 解决方案机构 + desc: + other: 有150个答案被接受。 + nice_answer: + name: + other: 好答案 + desc: + other: 回答得分为10或以上。 + good_answer: + name: + other: 好答案 + desc: + other: 回答得分为25或更多。 + great_answer: + name: + other: 优秀答案 + desc: + other: 回答得分为50或以上。 + nice_question: + name: + other: 好问题 + desc: + other: 问题得分为10或以上。 + good_question: + name: + other: 好问题 + desc: + other: 问题得分为25或更多。 + great_question: + name: + other: 很棒的问题 + desc: + other: 问题得分为50或更多。 + popular_question: + name: + other: 热门问题 + desc: + other: 问题有 500 个浏览量。 + notable_question: + name: + other: 值得关注问题 + desc: + other: 问题有 1,000 个浏览量。 + famous_question: + name: + other: 著名的问题 + desc: + other: 问题有 5,000 个浏览量。 + popular_link: + name: + other: 热门链接 + desc: + other: 发布了一个带有50个点击的外部链接。 + hot_link: + name: + other: 热门链接 + desc: + other: 发布了一个带有300个点击的外部链接。 + famous_link: + name: + other: 著名链接 + desc: + other: 发布了一个带有100个点击的外部链接。 + default_badge_groups: + getting_started: + name: + other: 完成初始化 + community: + name: + other: Community 专题 + posting: + name: + other: 发帖 +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何排版 + desc: >- +
  • 引用问题或答案: #4

  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签维基 + create_tag: 创建标签 + edit_tag: 编辑标签 + ask_a_question: 创建问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 帖子包含 + settings: 设置 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已被封禁 + admin: 后台管理 + change_email: 修改邮箱 + install: Answer 安装 + upgrade: Answer 升级 + maintenance: 网站维护 + users: 用户 + oauth_callback: 处理中 + http_404: HTTP 错误 404 + http_50X: HTTP 错误 500 + http_403: HTTP 错误 403 + logout: 退出 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + new_alerts: 新通知 + all_read: 全部标记为已读 + show_more: 显示更多 + someone: 有人 + inbox_type: + all: 全部 + posts: 帖子 + invites: 邀请 + votes: 投票 + answer: 回答 + question: 问题 + badge_award: 徽章 + suspended: + title: 你的账号账号已被封禁 + until_time: "你的账号被封禁直到 {{ time }}。" + forever: 你的账号已被永久封禁。 + end: 你违反了我们的社区准则。 + contact_us: 联系我们 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: 实体关系图 + user_defined_diagram: 用户自定义图表 + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 块级公式 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图像文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 文件大小不能超过 {{size}} MB。 + desc: + label: 描述 + tab_url: 图片地址 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中 + indent: + text: 缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有序列表 + unordered_list: + text: 无序列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + file: + text: 附件 + not_supported: "不支持的文件类型。请尝试上传其他类型的文件如: {{file_type}}。" + max_size: "上传文件超过 {{size}} MB。" + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 我举报这篇帖子的原因是... + close_title: 我关闭这篇帖子的原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + not_a_url: URL 格式不正确。 + url_not_match: URL 来源与当前网站不匹配。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称 + msg: + empty: 显示名称不能为空。 + range: 显示名称不能超过 35 个字符。 + slug_name: + label: URL 固定链接 + desc: URL 固定链接不能超过 35 个字符。 + msg: + empty: URL 固定链接不能为空。 + range: URL 固定链接不能超过 35 个字符。 + character: URL 固定链接包含非法字符。 + desc: + label: 描述 + revision: + label: 编辑历史 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_cancel: 取消 + btn_submit: 提交 + btn_post: 发布新标签 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + history: 历史 + synonyms: + title: 同义词 + text: 以下标签将被重置到 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签将被重置到 + delete: + title: 删除标签 + tip_with_posts: >- +

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

+ tip_with_synonyms: >- +

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

+ tip: 确定要删除吗? + close: 关闭 + merge: + title: 合并标签 + source_tag_title: 源标签 + source_tag_description: 源标签及其相关数据将重新映射到目标标签。 + target_tag_title: 目标标签 + target_tag_description: 合并后将在这两个标签之间将创建一个同义词。 + no_results: 没有匹配的标签 + btn_submit: 提交 + btn_close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + default_first_reason: 添加标签 + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM 月 DD 日 + long_date_with_year: "YYYY 年 MM 月 DD 日" + long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" + now: 刚刚 + x_seconds_ago: "{{count}} 秒前" + x_minutes_ago: "{{count}} 分钟前" + x_hours_ago: "{{count}} 小时前" + hour: 小时 + day: 天 + hours: 小时 + days: 日 + month: 月 + months: 月 + year: 年 + reaction: + heart: 爱心 + smile: 微笑 + frown: 愁 + btn_label: 添加或删除回应。 + undo_emoji: 撤销 {{ emoji }} 回应 + react_emoji: 用 {{ emoji }} 回应 + unreact_emoji: 撤销 {{ emoji }} + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存更改 + btn_cancel: 取消 + show_more: "{{count}} 条剩余评论" + tip_question: >- + 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 + tip_answer: >- + 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + tip_vote: 它给帖子添加了一些有用的内容 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + default_first_reason: 添加答案 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + feedback: + characters: 内容长度至少 6 个字符 + edit_summary: + label: 编辑摘要 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名称过滤 + no_desc: 此标签无描述。 + more: 更多 + wiki: 维基 + ask: + title: 创建问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + default_first_reason: 创建问题 + similar_questions: 相似问题 + form: + fields: + revision: + label: 修订版本 + title: + label: 标题 + placeholder: 你的主题是什么?请具体说明。 + msg: + empty: 标题不能为空。 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空。 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 回答自己的问题 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: "描述您的内容是关于什么,至少需要一个标签。" + no_result: 没有匹配的标签 + tag_required_text: 必选标签(至少一个) + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + badges: 徽章 + profile: 用户主页 + setting: 账号设置 + logout: 退出 + admin: 后台管理 + review: 审查 + bookmark: 收藏夹 + moderation: 管理 + search: + placeholder: 搜索 + footer: + build_on: >- + 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. + upload_img: + name: 更改 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 验证码不能为空。 + inactive: + first: >- + 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 + info: "如果没有收到,请检查你的垃圾邮件文件夹。" + another: >- + 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 + btn_name: 重新发送激活邮件 + change_btn_name: 更改邮箱 + msg: + empty: 不能为空。 + resend_email: + url_label: 确定要重新发送激活邮件吗? + url_text: 你也可以将上面的激活链接给该用户。 + login: + login_to_continue: 登录以继续 + info_sign: 没有账户?<1>注册 + info_login: 已经有账户?<1>登录 + agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 + forgot_pass: 忘记密码? + name: + label: 名字 + msg: + empty: 名字不能为空 + range: 名称长度必须在 2 至 30 个字符之间。 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: >- + 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + change_email: + btn_cancel: 取消 + btn_update: 更新电子邮件地址 + send_success: >- + 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 新的电子邮件地址 + msg: + empty: 邮箱不能为空。 + oauth: + connect: 连接到 {{ auth_name }} + remove: 移除 {{ auth_name }} + oauth_bind_email: + subtitle: 向你的账户添加恢复邮件地址。 + btn_update: 更新电子邮件地址 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空。 + modal_title: 邮箱已经存在。 + modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? + modal_cancel: 更改邮箱 + modal_confirm: 连接到已有账户 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: >- + 你已经成功更改密码;你将被重定向到登录页面。 + link_invalid: >- + 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空。 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + goto_modify: 前往修改 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + heading: 个人资料 + btn_name: 保存 + display_name: + label: 显示名称 + msg: 昵称不能为空。 + msg_range: 显示名称长度必须为 2-30 个字符。 + username: + label: 用户名 + caption: 用户可以通过 "@用户名" 来提及你。 + msg: 用户名不能为空 + msg_range: 显示名称长度必须为 2-30 个字符。 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + avatar: + label: 头像 + gravatar: Gravatar + gravatar_text: 你可以更改图像在 + custom: 自定义 + custom_text: 你可以上传你的图片。 + default: 系统 + msg: 请上传头像 + bio: + label: 关于我 + website: + label: 网站 + placeholder: "https://example.com" + msg: 网址格式不正确 + location: + label: 位置 + placeholder: "城市,国家" + notification: + heading: 邮件通知 + turn_on: 开启 + inbox: + label: 收件箱通知 + description: 你的提问有新的回答,评论,邀请回答和其他。 + all_new_question: + label: 所有新问题 + description: 获取所有新问题的通知。每周最多有50个问题。 + all_new_question_for_following_tags: + label: 所有关注标签的新问题 + description: 获取关注的标签下新问题通知。 + account: + heading: 账号 + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: >- + 邮件已发送。请根据指引完成验证。 + email: + label: 电子邮件地址 + new_email: + label: 新的电子邮件地址 + msg: 新邮箱不能为空。 + pass: + label: 当前密码 + msg: 密码不能为空。 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + heading: 界面 + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + my_logins: + title: 我的登录 + label: 使用这些账户登录或注册本网站。 + modal_title: 移除登录 + modal_content: 你确定要从账户里移除该登录? + modal_confirm_btn: 移除 + remove_success: 移除成功 + toast: + update: 更新成功 + update_password: 密码更新成功。 + flag_success: 感谢标记。 + forbidden_operate_self: 禁止对自己执行操作 + review: 您的修订将在审阅通过后显示。 + sent_success: 发送成功 + related_question: + title: 相似 + answers: 个回答 + linked_question: + title: 关联 + description: 帖子关联到 + no_linked_question: 没有与之关联的贴子。 + invite_to_answer: + title: 受邀人 + desc: 邀请你认为可能知道答案的人。 + invite: 邀请回答 + add: 添加人员 + search: 搜索人员 + question_detail: + action: 操作 + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 编辑于 + commented: 评论 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + follow_tip: 关注此问题以接收通知 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看类似问题。 + useful: 有用的 + question_useful: 它是有用和明确的 + question_un_useful: 它不明确或没用的 + question_bookmark: 收藏该问题 + answer_useful: 这是有用的 + answer_un_useful: 它是没有用的 + answers: + title: 个回答 + score: 评分 + newest: 最新 + oldest: 最旧 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + edit_answer: 编辑我的回答 + btn_name: 提交你的回答 + add_another_answer: 添加另一个回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info: >- +

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

+ empty: 回答内容不能为空。 + characters: 内容长度至少 6 个字符。 + tips: + header_1: 感谢你的回答 + li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 + li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 + header_2: 但是 请避免... + li2_1: 请求帮助,寻求澄清,或答复其他答案。 + reopen: + confirm_btn: 重新打开 + title: 重新打开这个帖子 + content: 确定要重新打开吗? + list: + confirm_btn: 列表显示 + title: 列表中显示这个帖子 + content: 确定要列表中显示这个帖子吗? + unlist: + confirm_btn: 列表隐藏 + title: 从列表中隐藏这个帖子 + content: 确定要从列表中隐藏这个帖子吗? + pin: + title: 置顶该帖子 + content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 + confirm_btn: 置顶 + delete: + title: 删除 + question: >- + 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_answer_deleted: 该回答已被删除 + undelete_title: 撤销删除本帖 + undelete_desc: 你确定你要撤销删除吗? + btns: + confirm: 确认 + cancel: 取消 + edit: 编辑 + save: 保存 + delete: 删除 + undelete: 撤消删除 + list: 列表显示 + unlist: 列表隐藏 + unlisted: 已隐藏 + login: 登录 + signup: 注册 + logout: 退出 + verify: 验证 + create: 创建 + approve: 批准 + reject: 拒绝 + skip: 跳过 + discard_draft: 丢弃草稿 + pinned: 已置顶 + all: 全部 + question: 问题 + answer: 回答 + comment: 评论 + refresh: 刷新 + resend: 重新发送 + deactivate: 取消激活 + active: 激活 + suspend: 封禁 + unsuspend: 解禁 + close: 关闭 + reopen: 重新打开 + ok: 确定 + light: 浅色 + dark: 深色 + system_setting: 跟随系统 + default: 默认 + reset: 重置 + tag: 标签 + post_lowercase: 帖子 + filter: 筛选 + ignore: 忽略 + submit: 提交 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + deleted_permanently: 永久删除 + pending: 等待处理 + more: 更多 + view: 浏览量 + card: 卡片 + compact: 紧凑 + display_below: 在下方显示 + always_display: 总是显示 + or: 或者 + back_sites: 返回网站 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: "{{count}} 个结果" + counts_loading: "... 个结果" + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + more: 更多 + tips: + title: 高级搜索提示 + tag: "<1>[tag] 在指定标签中搜索" + user: "<1>user:username 根据作者搜索" + answer: "<1>answers:0 搜索未回答的问题" + score: "<1>score:3 评分 3+ 的帖子" + question: "<1>is:question 搜索问题" + is_answer: "<1>is:answer 搜索回答" + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享到... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 X + cannot_vote_for_self: 你不能给自己的帖子投票。 + modal_confirm: + title: 发生错误... + delete_permanently: + title: 永久删除 + content: 您确定要永久删除吗? + account_result: + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + oops: 糟糕! + invalid: 您使用的链接不再有效。 + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: >- + 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + unsubscribe: + page_title: 退订 + success_title: 退订成功 + success_desc: 您已成功退订,并且将不会再收到我们的邮件。 + link: 更改设置 + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 关注标签来筛选你的问题列表。 + hot_questions: 热门问题 + all_questions: 全部问题 + x_questions: "{{ count }} 个问题" + x_answers: "{{ count }} 个回答" + x_posts: "{{ count }} 个帖子" + questions: 问题 + answers: 回答 + newest: 最新 + active: 活跃 + hot: 热门 + frequent: 频繁的 + recommend: 推荐 + score: 评分 + unanswered: 未回答 + modified: 更新于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + badges: 徽章 + newest: 最新 + score: 评分 + edit_profile: 编辑资料 + visited_x_days: "已访问 {{ count }} 天" + viewed: 浏览次数 + joined: 加入于 + comma: "," + last_login: 上次登录 + about_me: 关于我 + about_me_empty: "// Hello, World!" + top_answers: 高分回答 + top_questions: 高分问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他选项卡? + content_empty: 未找到帖子。 + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + downvoted: 点踩 + mod_short: 版主 + mod_long: 版主 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + recent_badges: 最近的徽章 + install: + title: 安装 + next: 下一步 + done: 完成 + config_yaml_error: 无法创建 config.yaml 文件。 + lang: + label: 请选择一种语言 + db_type: + label: 数据库引擎 + db_username: + label: 用户名 + placeholder: root + msg: 用户名不能为空 + db_password: + label: 密码 + placeholder: root + msg: 密码不能为空 + db_host: + label: 数据库主机 + placeholder: "db:3306" + msg: 数据库地址不能为空 + db_name: + label: 数据库名 + placeholder: 回答 + msg: 数据库名称不能为空。 + db_file: + label: 数据库文件 + placeholder: /data/answer.db + msg: 数据库文件不能为空。 + ssl_enabled: + label: 启用 SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL 模式 + ssl_root_cert: + placeholder: sslrootcert文件路径 + msg: sslrootcert 文件的路径不能为空 + ssl_cert: + placeholder: sslcert文件路径 + msg: sslcert 文件的路径不能为空 + ssl_key: + placeholder: sslkey 文件路径 + msg: sslcert 文件的路径不能为空 + config_yaml: + title: 创建 config.yaml + label: 已创建 config.yaml 文件。 + desc: >- + 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 + info: 完成后,点击“下一步”按钮。 + site_information: 站点信息 + admin_account: 管理员账号 + site_name: + label: 站点名称 + msg: 站点名称不能为空。 + msg_max_length: 站点名称长度不得超过 30 个字符。 + site_url: + label: 网站网址 + text: 此网站的网址。 + msg: + empty: 网址不能为空。 + incorrect: 网址格式不正确。 + max_length: 网址长度不得超过 512 个字符。 + contact_email: + label: 联系邮箱 + text: 负责本网站的主要联系人的电子邮件地址。 + msg: + empty: 联系人邮箱不能为空。 + incorrect: 联系人邮箱地址不正确。 + login_required: + label: 私有的 + switch: 需要登录 + text: 只有登录用户才能访问这个社区。 + admin_name: + label: 名字 + msg: 名字不能为空。 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + msg_max_length: 名称长度必须在 2 至 30 个字符之间。 + admin_password: + label: 密码 + text: >- + 您需要此密码才能登录。请将其存储在一个安全的位置。 + msg: 密码不能为空。 + msg_min_length: 密码必须至少 8 个字符长。 + msg_max_length: 密码长度不能超过 32 个字符。 + admin_confirm_password: + label: "确认密码" + text: "请重新输入您的密码以确认。" + msg: "确认密码不一致。" + admin_email: + label: 邮箱 + text: 您需要此电子邮件才能登录。 + msg: + empty: 邮箱不能为空。 + incorrect: 邮箱格式不正确。 + ready_title: 您的网站已准备好 + ready_desc: >- + 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 + good_luck: "玩得愉快,祝你好运!" + warn_title: 警告 + warn_desc: >- + 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 + install_now: 您可以尝试 <1>现在安装。 + installed: 已安裝 + installed_desc: >- + 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 + db_failed: 数据连接异常! + db_failed_desc: >- + 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 + counts: + views: 次浏览 + votes: 个点赞 + answers: 个回答 + accepted: 已被采纳 + page_error: + http_error: HTTP 错误 {{ code }} + desc_403: 您无权访问此页面。 + desc_404: 很抱歉,此页面不存在。 + desc_50X: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 返回首页 + page_maintenance: + desc: "我们正在进行维护,我们将很快回来。" + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + badges: 徽章 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + branding: 品牌 + legal: 法律条款 + write: 撰写 + tos: 服务条款 + privacy: 隐私政策 + seo: SEO + customize: 自定义 + themes: 主题 + login: 登录 + privileges: 特权 + plugins: 插件 + installed_plugins: 已安装插件 + apperance: 外观 + website_welcome: 欢迎来到 {{site_name}} + user_center: + login: 登录 + qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 + login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 + badges: + modal: + title: 恭喜 + content: 你赢得了一个新徽章。 + close: 关闭 + confirm: 查看徽章 + title: 徽章 + awarded: 授予 + earned_×: 以获得 ×{{ number }} + ×_awarded: "{{ number }} 得到" + can_earn_multiple: 你可以多次获得 + earned: 获得 + admin: + admin_header: + title: 后台管理 + dashboard: + title: 后台管理 + welcome: 欢迎来到管理后台! + site_statistics: 站点统计 + questions: "问题:" + resolved: "已解决:" + unanswered: "未回答:" + answers: "回答:" + comments: "评论:" + votes: "投票:" + users: "用户:" + flags: "举报:" + reviews: "审查:" + site_health: 网站健康 + version: "版本" + https: "HTTPS:" + upload_folder: "上传文件夹:" + run_mode: "运行模式:" + private: 私有 + public: 公开 + smtp: "SMTP:" + timezone: "时区:" + system_info: 系统信息 + go_version: "Go版本:" + database: "数据库:" + database_size: "数据库大小:" + storage_used: "已用存储空间:" + uptime: "运行时间:" + links: 链接 + plugins: 插件 + github: GitHub + blog: 博客 + contact: 联系 + forum: 论坛 + documents: 文档 + feedback: 用户反馈 + support: 帮助 + review: 审查 + config: 配置 + update_to: 更新到 + latest: 最新版本 + check_failed: 校验失败 + "yes": "是" + "no": "否" + not_allowed: 拒绝 + allowed: 允许 + enabled: 已启用 + disabled: 停用 + writable: 可写 + not_writable: 不可写 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + flagged_type: 标记了 {{ type }} + created: 创建于 + action: 操作 + review: 审查 + user_role_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + new_password_modal: + title: 设置新密码 + form: + fields: + password: + label: 密码 + text: 用户将被退出,需要再次登录。 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + edit_profile_modal: + title: 编辑资料 + form: + fields: + display_name: + label: 显示名称 + msg_range: 显示名称长度必须为 2-30 个字符。 + username: + label: 用户名 + msg_range: 用户名长度必须为 2-30 个字符。 + email: + label: 电子邮件地址 + msg_invalid: 无效的邮箱地址 + edit_success: 修改成功 + btn_cancel: 取消 + btn_submit: 提交 + user_modal: + title: 添加新用户 + form: + fields: + users: + label: 批量添加用户 + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: 用逗号分隔“name, email, password”,每行一个用户。 + msg: "请输入用户的邮箱,每行一个。" + display_name: + label: 显示名称 + msg: 显示名称长度必须为 2-30 个字符 + email: + label: 邮箱 + msg: 邮箱无效。 + password: + label: 密码 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + suspend_until: 封禁到期 + status: 状态 + role: 角色 + action: 操作 + change: 更改 + all: 全部 + staff: 工作人员 + more: 更多 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + Moderator: 版主 + Admin: 管理员 + User: 用户 + filter: + placeholder: "按名称筛选,用户:id" + set_new_password: 设置新密码 + edit_profile: 编辑资料 + change_status: 更改状态 + change_role: 更改角色 + show_logs: 显示日志 + add_user: 添加用户 + deactivate_user: + title: 停用用户 + content: 未激活的用户必须重新验证他们的邮箱。 + delete_user: + title: 删除此用户 + content: 确定要删除此用户?此操作无法撤销! + remove: 移除内容 + label: 删除所有问题、 答案、 评论等 + text: 如果你只想删除用户账户,请不要选中此项。 + suspend_user: + title: 挂起此用户 + content: 被封禁的用户将无法登录。 + label: 用户将被封禁多长时间? + forever: 永久 + questions: + page_title: 问题 + unlisted: 已隐藏 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + pending: 等待处理 + filter: + placeholder: "按标题过滤,问题:id" + answers: + page_title: 回答 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + filter: + placeholder: "按标题筛选,答案:id" + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: "站点的名称,作为站点的标题。" + site_url: + label: 网站网址 + msg: 网站网址不能为空。 + validate: 请输入一个有效的 URL。 + text: 此网站的地址。 + short_desc: + label: 简短站点描述 + msg: 简短网站描述不能为空。 + text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" + desc: + label: 站点描述 + msg: 网站描述不能为空。 + text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" + contact_email: + label: 联系邮箱 + msg: 联系人邮箱不能为空。 + validate: 联系人邮箱无效。 + text: 本网站的主要联系邮箱地址。 + check_update: + label: 软件更新 + text: 自动检查软件更新 + interface: + page_title: 界面 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + time_zone: + label: 时区 + msg: 时区不能为空。 + text: 选择一个与您相同时区的城市。 + avatar: + label: 默认头像 + text: 没有自定义头像的用户。 + gravatar_base_url: + label: Gravatar 根路径 URL + text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 + smtp: + page_title: SMTP + from_email: + label: 发件人邮箱 + msg: 发件人邮箱不能为空。 + text: 用于发送邮件的地址。 + from_name: + label: 发件人 + msg: 不能为空 + text: 发件人的名字。 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + tls: TLS + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试收件邮箱 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 测试收件邮箱无效 + smtp_authentication: + label: 启用身份验证 + title: SMTP 身份验证 + msg: 不能为空 + "yes": "是" + "no": "否" + branding: + page_title: 品牌 + logo: + label: 网站标志(Logo) + msg: 图标不能为空。 + text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 + mobile_logo: + label: 移动端 Logo + text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 + square_icon: + label: 方形图标 + msg: 方形图标不能为空。 + text: 用作元数据图标的基础的图像。最好是大于512x512。 + favicon: + label: 收藏夹图标 + text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 + legal: + page_title: 法律条款 + terms_of_service: + label: 服务条款 + text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" + privacy_policy: + label: 隐私政策 + text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" + external_content_display: + label: 外部内容 + text: "内容包括从外部网站嵌入的图像、视频和媒体。" + always_display: 总是显示外部内容 + ask_before_display: 在显示外部内容之前询问 + write: + page_title: 编辑 + restrict_answer: + title: 回答编辑 + label: 每个用户对于每个问题只能有一个回答 + text: "用户可以使用编辑按钮优化已有的回答" + recommend_tags: + label: 推荐标签 + text: "推荐标签将默认显示在下拉列表中。" + msg: + contain_reserved: "推荐标签不能包含保留标签" + required_tag: + title: 设置必填标签 + label: 设置“推荐标签”为必需的标签 + text: "每个新问题必须至少有一个推荐标签。" + reserved_tags: + label: 保留标签 + text: "只有版主才能使用保留的标签。" + image_size: + label: 最大图像大小 (MB) + text: "最大图像上传大小." + attachment_size: + label: 最大附件大小 (MB) + text: "最大附件文件上传大小。" + image_megapixels: + label: 最大图像兆像素 + text: "允许图像的最大兆位数。" + image_extensions: + label: 允许的图像后缀 + text: "允许图像显示的文件扩展名的列表,用英文逗号分隔。" + attachment_extensions: + label: 允许的附件后缀 + text: "允许上传的文件扩展名列表与英文逗号分开。警告:允许上传可能会导致安全问题。" + seo: + page_title: 搜索引擎优化 + permalink: + label: 固定链接 + text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 + robots: + label: robots.txt + text: 这将永久覆盖任何相关的网站设置。 + themes: + page_title: 主题 + themes: + label: 主题 + text: 选择一个现有主题。 + color_scheme: + label: 配色方案 + navbar_style: + label: 导航栏背景样式 + primary_color: + label: 主色调 + text: 修改您主题使用的颜色 + css_and_html: + page_title: CSS 与 HTML + custom_css: + label: 自定义 CSS + text: > + + head: + label: 头部 + text: > + + header: + label: 页眉 + text: > + + footer: + label: 页脚 + text: 这将在 </body> 之前插入。 + sidebar: + label: 侧边栏 + text: 这将插入侧边栏中。 + login: + page_title: 登录 + membership: + title: 会员 + label: 允许新注册 + text: 关闭以防止任何人创建新账户。 + email_registration: + title: 邮箱注册 + label: 允许邮箱注册 + text: 关闭以阻止任何人通过邮箱创建新账户。 + allowed_email_domains: + title: 允许的邮箱域 + text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 + private: + title: 非公开的 + label: 需要登录 + text: 只有登录用户才能访问这个社区。 + password_login: + title: 密码登录 + label: 允许使用邮箱和密码登录 + text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" + installed_plugins: + title: 已安装插件 + plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 + filter: + all: 全部 + active: 已启用 + inactive: 未启用 + outdated: 已过期 + plugins: + label: 插件 + text: 选择一个现有的插件。 + name: 名称 + version: 版本 + status: 状态 + action: 操作 + deactivate: 停用 + activate: 启用 + settings: 设置 + settings_users: + title: 用户 + avatar: + label: 默认头像 + text: 没有自定义头像的用户。 + gravatar_base_url: + label: Gravatar 根路径 URL + text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 + profile_editable: + title: 个人资料可编辑 + allow_update_display_name: + label: 允许用户修改显示名称 + allow_update_username: + label: 允许用户修改用户名 + allow_update_avatar: + label: 允许用户修改个人头像 + allow_update_bio: + label: 允许用户修改个人介绍 + allow_update_website: + label: 允许用户修改个人主页网址 + allow_update_location: + label: 允许用户更改位置 + privilege: + title: 特权 + level: + label: 级别所需声望 + text: 选择特权所需的声望值 + msg: + should_be_number: 输入必须是数字 + number_larger_1: 数字应该大于等于 1 + badges: + action: 操作 + active: 活跃的 + activate: 启用 + all: 全部 + awards: 奖项 + deactivate: 取消激活 + filter: + placeholder: 按名称筛选,或使用 badge:id + group: 组 + inactive: 未启用 + name: 名字 + show_logs: 显示日志 + status: 状态 + title: 徽章 + form: + optional: (选填) + empty: 不能为空 + invalid: 是无效的 + btn_submit: 保存 + not_found_props: "所需属性 {{ key }} 未找到。" + select: 选择 + page_review: + review: 评论 + proposed: 提案 + question_edit: 问题编辑 + answer_edit: 回答编辑 + tag_edit: '标签管理: 编辑标签' + edit_summary: 编辑备注 + edit_question: 编辑问题 + edit_answer: 编辑回答 + edit_tag: 编辑标签 + empty: 没有剩余的审核任务。 + approve_revision_tip: 您是否批准此修订? + approve_flag_tip: 您是否批准此举报? + approve_post_tip: 您是否批准此帖子? + approve_user_tip: 您是否批准此修订? + suggest_edits: 建议的编辑 + flag_post: 举报帖子 + flag_user: 举报用户 + queued_post: 排队的帖子 + queued_user: 排队用户 + filter_label: 类型 + reputation: 声望值 + flag_post_type: 举报这个帖子的类型是 {{ type }} + flag_user_type: 举报这个用户的类型是 {{ type }} + edit_post: 编辑帖子 + list_post: 文章列表 + unlist_post: 隐藏的帖子 + timeline: + undeleted: 取消删除 + deleted: 删除 + downvote: 反对 + upvote: 点赞 + accept: 采纳 + cancelled: 已取消 + commented: '评论:' + rollback: 回滚 + edited: 最后编辑于 + answered: 回答于 + asked: 提问于 + closed: 关闭 + reopened: 重新开启 + created: 创建于 + pin: 已置顶 + unpin: 取消置頂 + show: 已显示 + hide: 已隐藏 + title: "历史记录" + tag_title: "时间线" + show_votes: "显示投票" + n_or_a: N/A + title_for_question: "时间线" + title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" + title_for_tag: "时间线" + datetime: 日期时间 + type: 类型 + by: 由 + comment: 评论 + no_data: "空空如也" + users: + title: 用户 + users_with_the_most_reputation: 本周声望最高的用户 + users_with_the_most_vote: 本周投票最多的用户 + staffs: 我们的社区工作人员 + reputation: 声望值 + votes: 投票 + prompt: + leave_page: 确定要离开此页面? + changes_not_save: 您的更改尚未保存 + draft: + discard_confirm: 您确定要丢弃您的草稿吗? + messages: + post_deleted: 该帖子已被删除。 + post_cancel_deleted: 此帖子已被删除 + post_pin: 该帖子已被置顶。 + post_unpin: 该帖子已被取消置顶。 + post_hide_list: 此帖子已经从列表中隐藏。 + post_show_list: 该帖子已显示到列表中。 + post_reopen: 这个帖子已被重新打开. + post_list: 这个帖子已经被显示 + post_unlist: 这个帖子已经被隐藏 + post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 + post_closed: 此帖已关闭。 + answer_deleted: 该回答已被删除. + answer_cancel_deleted: 此答案已取消删除。 + change_user_role: 此用户的角色已被更改。 + user_inactive: 此用户已经处于未激活状态。 + user_normal: 此用户已经是正常的。 + user_suspended: 此用户已被封禁。 + user_deleted: 此用户已被删除 + badge_activated: 此徽章已被激活。 + badge_inactivated: 此徽章已被禁用。 + users_deleted: 这些用户已被删除。 + posts_deleted: 这些问题已被删除。 + answers_deleted: 这些答案已被删除。 + copy: 复制到剪贴板 + copied: 已复制 + external_content_warning: 外部图像/媒体未显示。 + -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" diff --git a/i18n/zh_TW.yaml b/i18n/zh_TW.yaml new file mode 100644 index 000000000..089b64a70 --- /dev/null +++ b/i18n/zh_TW.yaml @@ -0,0 +1,2341 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# The following fields are used for back-end +backend: + base: + success: + other: 成功。 + unknown: + other: 未知的錯誤。 + request_format_error: + other: 要求格式錯誤。 + unauthorized_error: + other: 未授權。 + database_error: + other: 資料伺服器錯誤。 + forbidden_error: + other: 已拒絕存取。 + duplicate_request_error: + other: 重複送出。 + action: + report: + other: 檢舉 + edit: + other: 編輯 + delete: + other: 删除 + close: + other: 關閉 + reopen: + other: 再次開啟。 + forbidden_error: + other: 已拒絕存取。 + pin: + other: 置頂 + hide: + other: 不公開 + unpin: + other: 取消置頂 + show: + other: 清單 + invite_someone_to_answer: + other: 編輯 + undelete: + other: 還原 + merge: + other: 合併 + role: + name: + user: + other: 使用者 + admin: + other: 管理員 + moderator: + other: 版主 + description: + user: + other: 預設沒有特別閱讀權限。 + admin: + other: 擁有存取此網站的全部權限。 + moderator: + other: 可以存取除了管理員設定以外的所有貼文。 + privilege: + level_1: + description: + other: Level 1 (less reputation required for private team, group) + level_2: + description: + other: Level 2 (low reputation required for startup community) + level_3: + description: + other: Level 3 (high reputation required for mature community) + level_custom: + description: + other: Custom Level + rank_question_add_label: + other: Ask question + rank_answer_add_label: + other: Write answer + rank_comment_add_label: + other: 寫留言 + rank_report_add_label: + other: Flag + rank_comment_vote_up_label: + other: Upvote comment + rank_link_url_limit_label: + other: Post more than 2 links at a time + rank_question_vote_up_label: + other: Upvote question + rank_answer_vote_up_label: + other: Upvote answer + rank_question_vote_down_label: + other: Downvote question + rank_answer_vote_down_label: + other: Downvote answer + rank_invite_someone_to_answer_label: + other: Invite someone to answer + rank_tag_add_label: + other: Create new tag + rank_tag_edit_label: + other: Edit tag description (need to review) + rank_question_edit_label: + other: Edit other's question (need to review) + rank_answer_edit_label: + other: Edit other's answer (need to review) + rank_question_edit_without_review_label: + other: Edit other's question without review + rank_answer_edit_without_review_label: + other: Edit other's answer without review + rank_question_audit_label: + other: Review question edits + rank_answer_audit_label: + other: Review answer edits + rank_tag_audit_label: + other: Review tag edits + rank_tag_edit_without_review_label: + other: Edit tag description without review + rank_tag_synonym_label: + other: Manage tag synonyms + email: + other: 電子郵件 + e_mail: + other: 電子郵件 + password: + other: 密碼 + pass: + other: 密碼 + old_pass: + other: 目前密碼 + original_text: + other: 此貼文 + email_or_password_wrong_error: + other: 電郵和密碼不匹配。 + error: + common: + invalid_url: + other: URL 無效。 + status_invalid: + other: 無效狀態。 + password: + space_invalid: + other: 密碼不能包含空白字元。 + admin: + cannot_update_their_password: + other: 你不能修改自己的密码。 + cannot_edit_their_profile: + other: You cannot modify your profile. + cannot_modify_self_status: + other: You cannot modify your status. + email_or_password_wrong: + other: 電郵和密碼不匹配。 + answer: + not_found: + other: 未發現答案。 + cannot_deleted: + other: 沒有刪除權限。 + cannot_update: + other: 沒有更新權限。 + question_closed_cannot_add: + other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. + comment: + edit_without_permission: + other: 不允許編輯留言。 + not_found: + other: 未發現留言。 + cannot_edit_after_deadline: + other: 這則留言時間過久,無法修改。 + content_cannot_empty: + other: Comment content cannot be empty. + email: + duplicate: + other: 這個電子郵件地址已被使用。 + need_to_be_verified: + other: 需驗證電子郵件地址。 + verify_url_expired: + other: 電子郵件地址驗證網址已過期,請重寄電子郵件。 + illegal_email_domain_error: + other: Email is not allowed from that email domain. Please use another one. + lang: + not_found: + other: 未找到語言檔。 + object: + captcha_verification_failed: + other: 驗證碼錯誤。 + disallow_follow: + other: 你不被允許追蹤。 + disallow_vote: + other: 你無法投票。 + disallow_vote_your_self: + other: 你不能為自己的貼文投票。 + not_found: + other: 找不到物件。 + verification_failed: + other: 驗證失敗。 + email_or_password_incorrect: + other: 電子郵件地址和密碼不匹配。 + old_password_verification_failed: + other: 舊密碼驗證失敗 + new_password_same_as_previous_setting: + other: 新密碼與先前的一樣。 + already_deleted: + other: 這則貼文已被刪除。 + meta: + object_not_found: + other: Meta object not found + question: + already_deleted: + other: This post has been deleted. + under_review: + other: Your post is awaiting review. It will be visible after it has been approved. + not_found: + other: 找不到問題。 + cannot_deleted: + other: 無刪除權限。 + cannot_close: + other: 無關閉權限。 + cannot_update: + other: 無更新權限。 + content_cannot_empty: + other: Content cannot be empty. + rank: + fail_to_meet_the_condition: + other: Reputation rank fail to meet the condition. + vote_fail_to_meet_the_condition: + other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote. + no_enough_rank_to_operate: + other: You need at least {{.Rank}} reputation to do this. + report: + handle_failed: + other: 報告處理失敗。 + not_found: + other: 找不到報告。 + tag: + already_exist: + other: Tag already exists. + not_found: + other: 找不到標籤。 + recommend_tag_not_found: + other: Recommend tag is not exist. + recommend_tag_enter: + other: 請輸入至少一個必需的標籤。 + not_contain_synonym_tags: + other: 不應包含同義詞標籤。 + cannot_update: + other: 沒有權限更新。 + is_used_cannot_delete: + other: You cannot delete a tag that is in use. + cannot_set_synonym_as_itself: + other: 你不能將目前標籤的同義詞設定為本身。 + smtp: + config_from_name_cannot_be_email: + other: The from name cannot be a email address. + theme: + not_found: + other: 未找到主題。 + revision: + review_underway: + other: 目前無法編輯,有一個版本在審查佇列中。 + no_permission: + other: No permission to revise. + user: + external_login_missing_user_id: + other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator. + external_login_unbinding_forbidden: + other: Please set a login password for your account before you remove this login. + email_or_password_wrong: + other: + other: 電子郵箱和密碼不匹配。 + not_found: + other: 未找到使用者。 + suspended: + other: 該使用者已被停權。 + username_invalid: + other: 使用者名稱無效。 + username_duplicate: + other: 使用者名稱已被使用。 + set_avatar: + other: 大頭照設定錯誤。 + cannot_update_your_role: + other: 您不能修改自己的角色。 + not_allowed_registration: + other: Currently the site is not open for registration. + not_allowed_login_via_password: + other: Currently the site is not allowed to login via password. + access_denied: + other: Access denied + page_access_denied: + other: You do not have access to this page. + add_bulk_users_format_error: + other: "Error {{.Field}} format near '{{.Content}}' at line {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "The number of users you add at once should be in the range of 1-{{.MaxAmount}}." + status_suspended_forever: + other: "This user was suspended forever. This user doesn't meet a community guideline." + status_suspended_until: + other: "This user was suspended until {{.SuspendedUntil}}. This user doesn't meet a community guideline." + status_deleted: + other: "This user was deleted." + status_inactive: + other: "This user is inactive." + config: + read_config_failed: + other: 讀取組態失敗 + database: + connection_failed: + other: 資料庫連線失敗 + create_table_failed: + other: 表建立失敗 + install: + create_config_failed: + other: 無法建立 config.yaml 檔。 + upload: + unsupported_file_format: + other: 不支援的檔案格式。 + site_info: + config_not_found: + other: Site config not found. + badge: + object_not_found: + other: Badge object not found + reason: + spam: + name: + other: 垃圾訊息 + desc: + other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic. + rude_or_abusive: + name: + other: rude or abusive + desc: + other: "A reasonable person would find this content inappropriate for respectful discourse." + a_duplicate: + name: + other: a duplicate + desc: + other: This question has been asked before and already has an answer. + placeholder: + other: Enter the existing question link + not_a_answer: + name: + other: not an answer + desc: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question,or deleted altogether." + no_longer_needed: + name: + other: no longer needed + desc: + other: This comment is outdated, conversational or not relevant to this post. + something: + name: + other: something else + desc: + other: This post requires staff attention for another reason not listed above. + placeholder: + other: Let us know specifically what you are concerned about + community_specific: + name: + other: a community-specific reason + desc: + other: This question doesn't meet a community guideline. + not_clarity: + name: + other: needs details or clarity + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + looks_ok: + name: + other: looks OK + desc: + other: This post is good as-is and not low quality. + needs_edit: + name: + other: needs edit, and I did it + desc: + other: Improve and correct problems with this post yourself. + needs_close: + name: + other: 需關閉 + desc: + other: A closed question can't answer, but still can edit, vote and comment. + needs_delete: + name: + other: needs delete + desc: + other: This post will be deleted. + question: + close: + duplicate: + name: + other: 垃圾訊息 + desc: + other: 此問題以前就有人問過,而且已經有了答案。 + guideline: + name: + other: 一个社群特定原因 + desc: + other: 此問題不符合社群準則。 + multiple: + name: + other: 需要細節或明晰 + desc: + other: This question currently includes multiple questions in one. It should focus on one problem only. + other: + name: + other: 其他原因 + desc: + other: 這個帖子需要上面沒有列出的另一個原因。 + operation_type: + asked: + other: 提問於 + answered: + other: 回答於 + modified: + other: 修改於 + deleted_title: + other: Deleted question + questions_title: + other: Questions + tag: + tags_title: + other: Tags + no_description: + other: The tag has no description. + notification: + action: + update_question: + other: 更新了問題 + answer_the_question: + other: 回答了問題 + update_answer: + other: 更新了答案 + accept_answer: + other: 已接受的回答 + comment_question: + other: 留言了問題 + comment_answer: + other: 留言了答案 + reply_to_you: + other: 回覆了你 + mention_you: + other: 提到了你 + your_question_is_closed: + other: 你的問題已被關閉 + your_question_was_deleted: + other: 你的問題已被刪除 + your_answer_was_deleted: + other: 你的答案已被刪除 + your_comment_was_deleted: + other: 你的留言已被刪除 + up_voted_question: + other: upvoted question + down_voted_question: + other: downvoted question + up_voted_answer: + other: upvoted answer + down_voted_answer: + other: downvoted answer + up_voted_comment: + other: upvoted comment + invited_you_to_answer: + other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] Confirm your new email address" + body: + other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} answered your question" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
I think you may know the answer.

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} commented on your post" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + new_question: + title: + other: "[{{.SiteName}}] New question: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.

\n\nUnsubscribe" + pass_reset: + title: + other: "[{{.SiteName }}] Password reset" + body: + other: "Somebody asked to reset your password on {{.SiteName}}.

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + register: + title: + other: "[{{.SiteName}}] Confirm your new account" + body: + other: "Welcome to {{.SiteName}}!

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + test: + title: + other: "[{{.SiteName}}] Test Email" + body: + other: "This is a test email.\n

\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen." + action_activity_type: + upvote: + other: upvote + upvoted: + other: upvoted + downvote: + other: downvote + downvoted: + other: downvoted + accept: + other: 採納 + accepted: + other: 已採納 + edit: + other: 編輯 + review: + queued_post: + other: Queued post + flagged_post: + other: Flagged post + suggested_post_edit: + other: Suggested edits + reaction: + tooltip: + other: "{{ .Names }} and {{ .Count }} more..." + badge: + default_badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out profile information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: 編輯者 + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: 首個連結 + desc: + other: First added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: 閱讀者 + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: 歡迎 + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: 感謝 + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何設定文字格式 + desc: >- +
  • mention a post: #post_id

  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: 上一頁 + next: 下一頁 + page_title: + question: 問題 + questions: 問題 + tag: 標籤 + tags: 標籤 + tag_wiki: 標籤 wiki + create_tag: Create Tag + edit_tag: 編輯標籤 + ask_a_question: Create Question + edit_question: 編輯問題 + edit_answer: 編輯回答 + search: 搜尋 + posts_containing: 包含的貼文 + settings: 設定 + notifications: 通知 + login: 登入 + sign_up: 註冊 + account_recovery: 帳號恢復 + account_activation: 帳號啟用 + confirm_email: 確認電子郵件 + account_suspended: 帳號已被停權 + admin: 後台管理 + change_email: 修改電子郵件 + install: Answer 安裝 + upgrade: Answer 升級 + maintenance: 網站維護 + users: 使用者 + oauth_callback: Processing + http_404: HTTP 錯誤 404 + http_50X: HTTP 錯誤 500 + http_403: HTTP 錯誤 403 + logout: 登出 + notifications: + title: 通知 + inbox: 收件夾 + achievement: 成就 + new_alerts: New alerts + all_read: 全部標記為已讀 + show_more: 顯示更多 + someone: Someone + inbox_type: + all: 所有 + posts: Posts + invites: Invites + votes: Votes + answer: Answer + question: Question + badge_award: Badge + suspended: + title: 您的帳號已被停權 + until_time: "你的帳號被停權至{{ time }}。" + forever: 你的帳號已被永久停權。 + end: 違反了我們的社群準則。 + contact_us: Contact us + editor: + blockquote: + text: 引用 + bold: + text: 粗體 + chart: + text: 圖表 + flow_chart: 流程圖 + sequence_diagram: 時序圖 + class_diagram: 類圖 + state_diagram: 狀態圖 + entity_relationship_diagram: 實體關係圖 + user_defined_diagram: 用戶自定義圖表 + gantt_chart: 甘特圖 + pie_chart: 圓餅圖 + code: + text: 代碼示例 + add_code: 添加代碼示例 + form: + fields: + code: + label: 代碼塊 + msg: + empty: 代碼不能為空 + language: + label: 語言 + placeholder: 自動偵測 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 內聯公式 + block: 公式塊 + heading: + text: 標題 + options: + h1: 標題 1 + h2: 標題 2 + h3: 標題 3 + h4: 標題 4 + h5: 標題 5 + h6: 標題 6 + help: + text: 幫助 + hr: + text: Horizontal rule + image: + text: 圖片 + add_image: 添加圖片 + tab_image: 上傳圖片 + form_image: + fields: + file: + label: 圖檔 + btn: 選擇圖片 + msg: + empty: 文件不能為空。 + only_image: 只能上傳圖片文件。 + max_size: File size cannot exceed {{size}} MB. + desc: + label: 圖片描述 + tab_url: 圖片地址 + form_url: + fields: + url: + label: 圖片地址 + msg: + empty: 圖片地址不能為空 + name: + label: 圖片描述 + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上傳中... + indent: + text: 增加縮排 + outdent: + text: 減少縮排 + italic: + text: 斜體 + link: + text: 超連結 + add_link: 添加超連結 + form: + fields: + url: + label: 連結 + msg: + empty: 連結不能為空。 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: Numbered list + unordered_list: + text: Bulleted list + table: + text: 表格 + heading: 表頭 + cell: 單元格 + file: + text: Attach files + not_supported: "Don’t support that file type. Try again with {{file_type}}." + max_size: "Attach files size cannot exceed {{size}} MB." + close_modal: + title: 關閉原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能為空。 + msg: + empty: 請選擇一個原因。 + report_modal: + flag_title: 報告為... + close_title: 關閉原因是... + review_question_title: 審核問題 + review_answer_title: 審核回答 + review_comment_title: 審核評論 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能為空 + msg: + empty: 請選擇一個原因。 + not_a_url: URL format is incorrect. + url_not_match: URL origin does not match the current website. + tag_modal: + title: 創建新標籤 + form: + fields: + display_name: + label: Display name + msg: + empty: 顯示名稱不能為空。 + range: 顯示名稱不能超過 35 個字符。 + slug_name: + label: URL slug + desc: URL slug up to 35 characters. + msg: + empty: URL 固定連結不能為空。 + range: URL 固定連結不能超過 35 個字元。 + character: URL 固定連結包含非法字元。 + desc: + label: 描述 + revision: + label: Revision + edit_summary: + label: Edit summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, improved formatting) + btn_cancel: 取消 + btn_submit: 提交 + btn_post: Post new tag + tag_info: + created_at: 創建於 + edited_at: 編輯於 + history: 歷史 + synonyms: + title: 同義詞 + text: 以下標籤等同於 + empty: 此標籤目前沒有同義詞。 + btn_add: 添加同義詞 + btn_edit: 編輯 + btn_save: 儲存 + synonyms_text: 以下標籤等同於 + delete: + title: 刪除標籤 + tip_with_posts: >- +

We do not allow deleting tag with posts.

Please remove this tag from the posts first.

+ tip_with_synonyms: >- +

We do not allow deleting tag with synonyms.

Please remove the synonyms from this tag first.

+ tip: 你確定要刪除嗎? + close: 關閉 + merge: + title: Merge tag + source_tag_title: Source tag + source_tag_description: The source tag and its associated data will be remapped to the target tag. + target_tag_title: Target tag + target_tag_description: A synonym between these two tags will be created after merging. + no_results: No tags matched + btn_submit: 送出 + btn_close: 關閉 + edit_tag: + title: 編輯標籤 + default_reason: 編輯標籤 + default_first_reason: Add tag + btn_save_edits: 儲存更改 + btn_cancel: 取消 + dates: + long_date: MM月DD日 + long_date_with_year: "YYYY年MM月DD日" + long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" + now: 剛剛 + x_seconds_ago: "{{count}} 秒前" + x_minutes_ago: "{{count}} 分鐘前" + x_hours_ago: "{{count}} 小時前" + hour: 小時 + day: 天 + hours: hours + days: days + month: month + months: months + year: year + reaction: + heart: heart + smile: smile + frown: frown + btn_label: add or remove reactions + undo_emoji: undo {{ emoji }} reaction + react_emoji: react with {{ emoji }} + unreact_emoji: unreact with {{ emoji }} + comment: + btn_add_comment: 添加評論 + reply_to: 回復 + btn_reply: 回復 + btn_edit: 編輯 + btn_delete: 刪除 + btn_flag: 舉報 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: "{{count}} 條剩餘評論" + tip_question: >- + 通过評論询问更多问题或提出改進建議。避免在評論中回答問題。 + tip_answer: >- + 使用評論回復其他用戶或通知他們进行更改。如果你要添加新的信息,請編輯你的帖子,而不是發表評論。 + tip_vote: It adds something useful to the post + edit_answer: + title: 編輯回答 + default_reason: 編輯回答 + default_first_reason: Add answer + form: + fields: + revision: + label: 編輯歷史 + answer: + label: 回答內容 + feedback: + characters: 內容必須至少6個字元長度。 + edit_summary: + label: Edit summary + placeholder: >- + 簡單描述更改原因 (錯別字、文字表達、格式等等) + btn_save_edits: 儲存更改 + btn_cancel: 取消 + tags: + title: 標籤 + sort_buttons: + popular: 熱門 + name: 名稱 + newest: Newest + button_follow: 關注 + button_following: 已關注 + tag_label: 個問題 + search_placeholder: 通過標籤名過濾 + no_desc: 此標籤無描述。 + more: 更多 + wiki: Wiki + ask: + title: Create Question + edit_title: 編輯問題 + default_reason: 編輯問題 + default_first_reason: Create question + similar_questions: 相似的問題 + form: + fields: + revision: + label: 編輯歷史 + title: + label: 標題 + placeholder: What's your topic? Be specific. + msg: + empty: 標題不能為空 + range: 標題最多 150 個字元 + body: + label: 正文 + msg: + empty: 正文不能爲空。 + tags: + label: 標籤 + msg: + empty: 標籤不能為空 + answer: + label: 回答內容 + msg: + empty: 回答內容不能為空 + edit_summary: + label: Edit summary + placeholder: >- + 簡單描述更改原因 (錯別字、文字表達、格式等等) + btn_post_question: 提出問題 + btn_save_edits: 儲存更改 + answer_question: 回答您自己的問題 + post_question&answer: 發布您的問題和答案 + tag_selector: + add_btn: 建立標籤 + create_btn: 建立新標籤 + search_tag: 搜尋標籤 + hint: "Describe what your content is about, at least one tag is required." + no_result: 沒有匹配的標籤 + tag_required_text: 必填標籤 (至少一個) + header: + nav: + question: 問題 + tag: 標籤 + user: 用戶 + badges: Badges + profile: 用戶主頁 + setting: 帳號設置 + logout: 登出 + admin: 後台管理 + review: 審查 + bookmark: Bookmarks + moderation: Moderation + search: + placeholder: 搜尋 + footer: + build_on: >- + Powered by <1> Apache Answer - the open-source software that powers Q&A communities.
Made with love © {{cc}}. + upload_img: + name: 更改 + loading: 讀取中... + pic_auth_code: + title: 驗證碼 + placeholder: 輸入上面的文字 + msg: + empty: 验证码不能為空 + inactive: + first: >- + 就差一步!我們寄送了一封啟用電子郵件到 {{mail}}。請按照郵件中的說明啟用您的帳戶。 + info: "如果沒有收到,請檢查您的垃圾郵件文件夾。" + another: >- + 我們向您發送了另一封啟用電子郵件,地址為 {{mail}}。它可能需要幾分鐘才能到達;請務必檢查您的垃圾郵件文件夾。 + btn_name: 重新發送啟用郵件 + change_btn_name: 更改郵箱 + msg: + empty: 不能為空 + resend_email: + url_label: Are you sure you want to resend the activation email? + url_text: You can also give the activation link above to the user. + login: + login_to_continue: 登入以繼續 + info_sign: 沒有帳戶?<1>註冊 + info_login: 已經有一個帳號?<1>登入 + agreements: 登入即表示您同意<1>隱私政策和<3>服務條款。 + forgot_pass: 忘記密碼? + name: + label: 名稱 + msg: + empty: 名稱不能為空 + range: Name must be between 2 to 30 characters in length. + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + email: + label: 郵箱 + msg: + empty: 郵箱不能為空 + password: + label: 密碼 + msg: + empty: 密碼不能為空 + different: 兩次輸入密碼不一致 + account_forgot: + page_title: 忘記密碼 + btn_name: 向我發送恢復郵件 + send_success: >- + 如果帳號與{{mail}}相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。 + email: + label: 郵箱 + msg: + empty: 郵箱不能為空 + change_email: + btn_cancel: 取消 + btn_update: 更新電子郵件地址 + send_success: >- + 如果帳號與{{mail}}相符,您應該很快就會收到一封電子郵件,說明如何重置您的密碼。 + email: + label: New email + msg: + empty: 郵箱不能為空 + oauth: + connect: Connect with {{ auth_name }} + remove: Remove {{ auth_name }} + oauth_bind_email: + subtitle: Add a recovery email to your account. + btn_update: Update email address + email: + label: Email + msg: + empty: Email cannot be empty. + modal_title: Email already existes. + modal_content: This email address already registered. Are you sure you want to connect to the existing account? + modal_cancel: Change email + modal_confirm: Connect to the existing account + password_reset: + page_title: 密碼重置 + btn_name: 重置我的密碼 + reset_success: >- + 你已經成功更改密碼,將返回登入頁面 + link_invalid: >- + 抱歉,此密碼重置連結已失效。也許是你已經重置過密碼了? + to_login: 前往登入頁面 + password: + label: 密碼 + msg: + empty: 密碼不能為空 + length: 密碼長度在8-32個字元之間 + different: 兩次輸入密碼不一致 + password_confirm: + label: Confirm new password + settings: + page_title: 設置 + goto_modify: Go to modify + nav: + profile: 我的資料 + notification: 通知 + account: 帳號 + interface: 界面 + profile: + heading: 個人資料 + btn_name: 保存 + display_name: + label: Display name + msg: 顯示名稱不能為空。 + msg_range: Display name must be 2-30 characters in length. + username: + label: 用戶名 + caption: 用戶之間可以通過 "@用戶名" 進行交互。 + msg: 用戶名不能為空 + msg_range: Username must be 2-30 characters in length. + character: '必須由 "a-z", "0-9", " - . _" 組成' + avatar: + label: Profile image + gravatar: 頭像 + gravatar_text: You can change image on + custom: 自定義 + custom_text: 您可以上傳您的圖片。 + default: 系統 + msg: 請上傳頭像 + bio: + label: About me + website: + label: 網站 + placeholder: "https://example.com" + msg: 網站格式不正確 + location: + label: 位置 + placeholder: "城市, 國家" + notification: + heading: 通知 + turn_on: Turn on + inbox: + label: Email notifications + description: Answers to your questions, comments, invites, and more. + all_new_question: + label: All new questions + description: Get notified of all new questions. Up to 50 questions per week. + all_new_question_for_following_tags: + label: All new questions for following tags + description: Get notified of new questions for following tags. + account: + heading: 帳號 + change_email_btn: 更改郵箱 + change_pass_btn: 更改密碼 + change_email_info: >- + 我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。 + email: + label: 電子郵件地址 + new_email: + label: 新電子郵件地址 + msg: 新電子郵件地址不能為空白。 + pass: + label: 目前密碼 + msg: Password cannot be empty. + password_title: 密碼 + current_pass: + label: Current password + msg: + empty: Current password cannot be empty. + length: 密碼長度必須在 8 至 32 之間 + different: 兩次輸入的密碼不匹配 + new_pass: + label: New password + pass_confirm: + label: 確認新密碼 + interface: + heading: 介面 + lang: + label: 介面語言 + text: 設定使用者介面語言,在重新整裡頁面後生效。 + my_logins: + title: 我的登入 + label: 使用這些帳號登入或註冊此網站。 + modal_title: 移除登入 + modal_content: Are you sure you want to remove this login from your account? + modal_confirm_btn: Remove + remove_success: Removed successfully + toast: + update: 更新成功 + update_password: 更改密碼成功。 + flag_success: 感謝您的標記 + forbidden_operate_self: 禁止自己操作 + review: 您的修訂將在審核通過後顯示。 + sent_success: Sent successfully + related_question: + title: Related + answers: 個回答 + linked_question: + title: Linked + description: Posts linked to + no_linked_question: No contents linked from this content. + invite_to_answer: + title: People Asked + desc: Invite people who you think might know the answer. + invite: Invite to answer + add: Add people + search: Search people + question_detail: + action: Action + Asked: 提問於 + asked: 提問於 + update: 修改於 + edit: 最後編輯於 + commented: commented + Views: 閱讀次數 + Follow: 關注 + Following: 已關注 + follow_tip: Follow this question to receive notifications + answered: 回答於 + closed_in: 關閉於 + show_exist: 顯示現有問題。 + useful: Useful + question_useful: It is useful and clear + question_un_useful: It is unclear or not useful + question_bookmark: Bookmark this question + answer_useful: It is useful + answer_un_useful: It is not useful + answers: + title: 個回答 + score: 評分 + newest: 最新 + oldest: Oldest + btn_accept: 採納 + btn_accepted: 已被採納 + write_answer: + title: 你的回答 + edit_answer: Edit my existing answer + btn_name: 提交你的回答 + add_another_answer: 添加另一個答案 + confirm_title: 繼續回答 + continue: 繼續 + confirm_info: >- +

您確定要添加一個新的回答嗎?

您可以使用编辑链接来完善和改进您现有的答案。

+ empty: 回答內容不能為空。 + characters: 內容必須至少6個字元長度。 + tips: + header_1: Thanks for your answer + li1_1: Please be sure to answer the question. Provide details and share your research. + li1_2: Back up any statements you make with references or personal experience. + header_2: But avoid ... + li2_1: Asking for help, seeking clarification, or responding to other answers. + reopen: + confirm_btn: Reopen + title: 重新打開這個貼文 + content: 確定要重新打開嗎? + list: + confirm_btn: List + title: List this post + content: Are you sure you want to list? + unlist: + confirm_btn: Unlist + title: Unlist this post + content: Are you sure you want to unlist? + pin: + title: Pin this post + content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists. + confirm_btn: Pin + delete: + title: 刪除此貼 + question: >- + 我們不建議刪除有回答的貼文。因為這樣做會使得後來的讀者無法從該問題中獲得幫助。

如果刪除過多有回答的貼文,你的帳號將會被禁止提問。你確定要刪除嗎? + answer_accepted: >- +

我們不建議刪除被採納的回答。因為這樣做會使得後來的讀者無法從該回答中獲得幫助。

如果刪除過多被採納的貼文,你的帳號將會被禁止回答任何提問。你確定要刪除嗎? + other: 你確定要刪除? + tip_answer_deleted: 此回答已被刪除 + undelete_title: Undelete this post + undelete_desc: Are you sure you wish to undelete? + btns: + confirm: 確認 + cancel: 取消 + edit: 編輯 + save: 儲存 + delete: 刪除 + undelete: 還原 + list: 清單 + unlist: Unlist + unlisted: Unlisted + login: 登入 + signup: 註冊 + logout: 登出 + verify: 驗證 + create: 建立 + approve: 核准 + reject: 拒絕 + skip: 略過 + discard_draft: Discard draft + pinned: Pinned + all: All + question: Question + answer: Answer + comment: Comment + refresh: Refresh + resend: Resend + deactivate: Deactivate + active: Active + suspend: Suspend + unsuspend: Unsuspend + close: Close + reopen: Reopen + ok: OK + light: Light + dark: Dark + system_setting: System setting + default: Default + reset: Reset + tag: Tag + post_lowercase: post + filter: Filter + ignore: Ignore + submit: Submit + normal: Normal + closed: Closed + deleted: Deleted + deleted_permanently: Deleted permanently + pending: Pending + more: More + view: View + card: Card + compact: Compact + display_below: Display below + always_display: Always display + or: or + back_sites: Back to sites + search: + title: 搜尋結果 + keywords: 關鍵詞 + options: 選項 + follow: 追蹤 + following: 已關注 + counts: "{{count}} 個結果" + counts_loading: "... Results" + more: 更多 + sort_btns: + relevance: 相關性 + newest: 最新的 + active: 活躍的 + score: 評分 + more: 更多 + tips: + title: 高級搜尋提示 + tag: "<1>[tag] 在指定標籤中搜尋" + user: "<1>user:username 根據作者搜尋" + answer: "<1>answers:0 搜尋未回答的問題" + score: "<1>score:3 得分為 3+ 的帖子" + question: "<1>is:question 只搜尋問題" + is_answer: "<1>is:answer 只搜尋回答" + empty: 找不到任何相關的內容。
請嘗試其他關鍵字,或者減少查找內容的長度。 + share: + name: 分享 + copy: 複製連結 + via: 分享在... + copied: 已複製 + facebook: 分享到 Facebook + twitter: Share to X + cannot_vote_for_self: You can't vote for your own post. + modal_confirm: + title: 發生錯誤... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? + account_result: + success: 你的帳號已通過驗證,即將返回首頁。 + link: 繼續訪問主頁 + oops: Oops! + invalid: The link you used no longer works. + confirm_new_email: 你的電子郵箱已更新 + confirm_new_email_invalid: >- + 抱歉,此驗證連結已失效。也許是你的郵箱已經成功更改了? + unsubscribe: + page_title: 退訂 + success_title: 取消訂閱成功 + success_desc: 您已成功從訂閱者清單中移除且不會在收到任何來自我們的郵件。 + link: 更改設置 + question: + following_tags: 已關注的標籤 + edit: 編輯 + save: 儲存 + follow_tag_tip: 按照標籤整理您的問題列表。 + hot_questions: 熱門問題 + all_questions: 全部問題 + x_questions: "{{ count }} 個問題" + x_answers: "{{ count }} 個回答" + x_posts: "{{ count }} Posts" + questions: 個問題 + answers: 回答 + newest: 最新的 + active: 活躍的 + hot: Hot + frequent: Frequent + recommend: Recommend + score: 評分 + unanswered: 未回答 + modified: 修改於 + answered: 回答於 + asked: 提問於 + closed: 已關閉 + follow_a_tag: 關注一個標籤 + more: 更多 + personal: + overview: 概覽 + answers: 回答 + answer: 回答 + questions: 問題 + question: 問題 + bookmarks: 書籤 + reputation: 聲望 + comments: 評論 + votes: 得票 + badges: Badges + newest: 最新 + score: 評分 + edit_profile: Edit profile + visited_x_days: "已造訪 {{ count }} 天" + viewed: 閱讀次數 + joined: 加入於 + comma: "," + last_login: 出現時間 + about_me: 關於我 + about_me_empty: "// 你好, 世界 !" + top_answers: 熱門回答 + top_questions: 熱門問題 + stats: 狀態 + list_empty: 沒有找到相關的內容。
試試看其他標籤? + content_empty: No posts found. + accepted: 已採納 + answered: 回答於 + asked: 提問於 + downvoted: downvoted + mod_short: MOD + mod_long: 管理員 + x_reputation: 聲望 + x_votes: 得票 + x_answers: 個回答 + x_questions: 個問題 + recent_badges: Recent Badges + install: + title: Installation + next: 下一步 + done: 完成 + config_yaml_error: 無法建立 config.yaml 檔。 + lang: + label: Please choose a language + db_type: + label: Database engine + db_username: + label: 用戶名 + placeholder: 根 + msg: 用戶名不能為空 + db_password: + label: 密碼 + placeholder: root + msg: 密碼不能為空 + db_host: + label: Database host + placeholder: "db: 3306" + msg: Database host cannot be empty. + db_name: + label: Database name + placeholder: 回答 + msg: Database name cannot be empty. + db_file: + label: Database file + placeholder: /data/answer.db + msg: Database file cannot be empty. + ssl_enabled: + label: Enable SSL + ssl_enabled_on: + label: On + ssl_enabled_off: + label: Off + ssl_mode: + label: SSL Mode + ssl_root_cert: + placeholder: sslrootcert file path + msg: Path to sslrootcert file cannot be empty + ssl_cert: + placeholder: sslcert file path + msg: Path to sslcert file cannot be empty + ssl_key: + placeholder: sslkey file path + msg: Path to sslkey file cannot be empty + config_yaml: + title: 創建 config.yaml + label: 已創建 config.yaml 文件。 + desc: >- + 您可以手動在 <1>/var/wwww/xxx/ 目錄中創建<1>config.yaml 文件並粘貼以下文本。 + info: 完成後點擊"下一步"按鈕。 + site_information: 網站資訊 + admin_account: 管理員帳戶 + site_name: + label: Site name + msg: Site name cannot be empty. + msg_max_length: Site name must be at maximum 30 characters in length. + site_url: + label: 網站 URL + text: 此網站的地址。 + msg: + empty: 網站URL不能為空。 + incorrect: 網站URL格式不正確。 + max_length: Site URL must be at maximum 512 characters in length. + contact_email: + label: Contact email + text: 負責本網站的主要聯絡人的電子郵件地址。 + msg: + empty: Contact email cannot be empty. + incorrect: Contact email incorrect format. + login_required: + label: Private + switch: Login required + text: Only logged in users can access this community. + admin_name: + label: 暱稱 + msg: 暱稱不能為空。 + character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"' + msg_max_length: Name must be between 2 to 30 characters in length. + admin_password: + label: 密碼 + text: >- + 您需要此密碼才能登入。請將其儲存在一個安全的位置。 + msg: 密碼不能為空。 + msg_min_length: Password must be at least 8 characters in length. + msg_max_length: Password must be at maximum 32 characters in length. + admin_confirm_password: + label: "Confirm Password" + text: "Please re-enter your password to confirm." + msg: "Confirm password does not match." + admin_email: + label: 郵箱 + text: 您需要此電子郵件才能登入。 + msg: + empty: 郵箱不能為空。 + incorrect: 郵箱格式不正確。 + ready_title: Your site is ready + ready_desc: >- + 如果你想改變更多的設定,請瀏覽<1>管理員部分;在網站選單中找到它。 + good_luck: "玩得愉快,祝您好運!" + warn_title: 警告 + warn_desc: >- + 檔案<1>config.yaml已存在。如果您需要重置此文件中的任何配置項,請先刪除它。 + install_now: 您可以嘗試<1>現在安裝。 + installed: 已安裝 + installed_desc: >- + 您似乎已經安裝過了。要重新安裝,請先清除舊的資料庫表。 + db_failed: 資料連接異常! + db_failed_desc: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down. + counts: + views: 觀看 + votes: 得票 + answers: 回答 + accepted: 已採納 + page_error: + http_error: HTTP 错误 {{ code }} + desc_403: You don't have permission to access this page. + desc_404: Unfortunately, this page doesn't exist. + desc_50X: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + desc: "我們正在維護中,很快就會回來。" + nav_menus: + dashboard: 後台管理 + contents: 內容 + questions: 問題 + answers: 回答 + users: 使用者管理 + badges: Badges + flags: 檢舉 + settings: 設定 + general: 一般 + interface: 介面 + smtp: SMTP + branding: 品牌 + legal: 法律條款 + write: 撰寫 + tos: 服務條款 + privacy: 隱私政策 + seo: SEO + customize: 自定義 + themes: 主題 + login: 登入 + privileges: Privileges + plugins: Plugins + installed_plugins: Installed Plugins + apperance: Appearance + website_welcome: Welcome to {{site_name}} + user_center: + login: Login + qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. + login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges + title: Badges + awarded: Awarded + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + earned: Earned + admin: + admin_header: + title: 後台管理 + dashboard: + title: 後台管理 + welcome: Welcome to Admin! + site_statistics: Site statistics + questions: "問題:" + resolved: "Resolved:" + unanswered: "Unanswered:" + answers: "回答:" + comments: "評論:" + votes: "投票:" + users: "Users:" + flags: "檢舉:" + reviews: "Reviews:" + site_health: Site health + version: "版本" + https: "HTTPS:" + upload_folder: "Upload folder:" + run_mode: "Running mode:" + private: Private + public: Public + smtp: "SMTP:" + timezone: "時區:" + system_info: System info + go_version: "Go version:" + database: "Database:" + database_size: "Database size:" + storage_used: "已用儲存空間:" + uptime: "運行時間:" + links: Links + plugins: Plugins + github: GitHub + blog: Blog + contact: Contact + forum: Forum + documents: 文件 + feedback: 用戶反饋 + support: 支持 + review: 審核 + config: 配置 + update_to: 更新到 + latest: 最新版本 + check_failed: 校驗失敗 + "yes": "是" + "no": "否" + not_allowed: 不允許 + allowed: 允許 + enabled: 已啟用 + disabled: 停用 + writable: Writable + not_writable: Not writable + flags: + title: 檢舉 + pending: 等待處理 + completed: 已完成 + flagged: 已標記 + flagged_type: Flagged {{ type }} + created: 創建於 + action: 操作 + review: 審核 + user_role_modal: + title: 更改用戶狀態為... + btn_cancel: 取消 + btn_submit: 提交 + new_password_modal: + title: Set new password + form: + fields: + password: + label: Password + text: The user will be logged out and need to login again. + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + edit_profile_modal: + title: Edit profile + form: + fields: + display_name: + label: Display name + msg_range: Display name must be 2-30 characters in length. + username: + label: Username + msg_range: Username must be 2-30 characters in length. + email: + label: Email + msg_invalid: Invalid Email Address. + edit_success: Edited successfully + btn_cancel: Cancel + btn_submit: Submit + user_modal: + title: Add new user + form: + fields: + users: + label: Bulk add user + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: Separate “name, email, password” with commas. One user per line. + msg: "Please enter the user's email, one per line." + display_name: + label: Display name + msg: Display name must be 2-30 characters in length. + email: + label: Email + msg: Email is not valid. + password: + label: Password + msg: Password must be at 8-32 characters in length. + btn_cancel: Cancel + btn_submit: Submit + users: + title: 用戶 + name: 名稱 + email: 郵箱 + reputation: 聲望 + created_at: Created time + delete_at: Deleted time + suspend_at: Suspended time + suspend_until: Suspend until + status: 狀態 + role: 角色 + action: 操作 + change: 更改 + all: 全部 + staff: 工作人員 + more: More + inactive: 不活躍 + suspended: 已停權 + deleted: 已刪除 + normal: 正常 + Moderator: 版主 + Admin: 管理員 + User: 用戶 + filter: + placeholder: "按名稱篩選,用戶:id" + set_new_password: 設置新密碼 + edit_profile: Edit profile + change_status: 更改狀態 + change_role: 更改角色 + show_logs: 顯示日誌 + add_user: 新增使用者 + deactivate_user: + title: Deactivate user + content: An inactive user must re-validate their email. + delete_user: + title: Delete this user + content: Are you sure you want to delete this user? This is permanent! + remove: Remove their content + label: Remove all questions, answers, comments, etc. + text: Don’t check this if you wish to only delete the user’s account. + suspend_user: + title: Suspend this user + content: A suspended user can't log in. + label: How long will the user be suspended for? + forever: Forever + questions: + page_title: 問題 + unlisted: Unlisted + post: 標題 + votes: 得票數 + answers: 回答 + created: 創建於 + status: 狀態 + action: 操作 + change: 更改 + pending: Pending + filter: + placeholder: "按標題過濾,問題:id" + answers: + page_title: 回答 + post: 發布 + votes: 得票數 + created: 創建於 + status: 狀態 + action: 操作 + change: 更改 + filter: + placeholder: "按名稱篩選,answer:id" + general: + page_title: 一般 + name: + label: Site name + msg: 不能為空 + text: "網站的名稱,如標題標籤中所用。" + site_url: + label: 網站網址 + msg: 網站網址不能為空。 + validate: 請輸入一個有效的 URL。 + text: 此網站的網址。 + short_desc: + label: Short site description + msg: 網站簡短描述不能為空。 + text: "簡短的描述,如主頁上的標題標籤所使用的那样。" + desc: + label: Site description + msg: 網站描述不能為空。 + text: "使用一句話描述本站,作為網站的描述(Html 的 meta 標籤)。" + contact_email: + label: Contact email + msg: 聯絡人信箱不能為空。 + validate: 聯絡人信箱無效。 + text: 負責本網站的主要聯絡人的電子郵件信箱。 + check_update: + label: Software updates + text: Automatically check for updates + interface: + page_title: 介面 + language: + label: Interface language + msg: 界面語言不能為空 + text: 設置用戶界面語言,在刷新頁面后生效。 + time_zone: + label: 時區 + msg: 時區不能為空。 + text: 選擇一個與您相同時區的城市。 + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar base URL + text: URL of the Gravatar provider's API base. Ignored when empty. + smtp: + page_title: SMTP + from_email: + label: From email + msg: 發件人電子郵件不能为空。 + text: 發送郵件的郵箱地址 + from_name: + label: From name + msg: 發件人名稱不能为空。 + text: 發件人的名稱 + smtp_host: + label: SMTP host + msg: SMTP 主機名稱不能為空。 + text: 郵件服務器 + encryption: + label: 加密 + msg: 加密不能為空。 + text: 對於大多數服務器,SSL 是推薦的選項。 + ssl: SSL + tls: TLS + none: 無 + smtp_port: + label: SMTP port + msg: SMTP 埠必須在 1 ~ 65535 之間。 + text: 郵件服務器的端口號。 + smtp_username: + label: SMTP username + msg: SMTP 用戶名不能為空。 + smtp_password: + label: SMTP password + msg: SMTP 密碼不能為空。 + test_email_recipient: + label: Test email recipients + text: 提供用於接收測試郵件的郵箱地址。 + msg: 測試郵件收件人無效 + smtp_authentication: + label: 啟用身份驗證 + title: SMTP authentication + msg: SMTP 身份驗證不能為空。 + "yes": "是" + "no": "否" + branding: + page_title: 品牌 + logo: + label: 標誌 + msg: 圖標不能為空。 + text: 在你的網站左上方的Logo圖標。使用一個高度為56,長寬比大於3:1的寬長方形圖像。如果留空,將顯示網站標題文本。 + mobile_logo: + label: Mobile logo + text: 在您網站的移動版本上使用的徽標。 使用高度為 56 的寬矩形圖像。如果留空,將使用“徽標”設置中的圖像。 + square_icon: + label: Square icon + msg: 方形圖示不能為空。 + text: 用作元數據圖標的基礎的圖像。最好是大於512x512。 + favicon: + label: 網站圖示 + text: 您網站的圖標。 要在 CDN 上正常工作,它必須是 png。 將調整為 32x32的大小。 如果留空,將使用“方形圖標”。 + legal: + page_title: 法律條款 + terms_of_service: + label: Terms of service + text: "您可以在此加入服務內容的條款。如果您已經在別處托管了文檔,請在這裡提供完整的URL。" + privacy_policy: + label: Privacy policy + text: "您可以在此加入隱私政策內容。如果您已經在別處托管了文檔,請在這裡提供完整的URL。" + external_content_display: + label: External content + text: "Content includes images, videos, and media embedded from external websites." + always_display: Always display external content + ask_before_display: Ask before displaying external content + write: + page_title: 編輯 + restrict_answer: + title: Answer write + label: Each user can only write one answer for each question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." + recommend_tags: + label: Recommend tags + text: "Recommend tags will show in the dropdown list by default." + msg: + contain_reserved: "recommended tags cannot contain reserved tags" + required_tag: + title: Set required tags + label: Set “Recommend tags” as required tags + text: "每個新問題必須至少有一個推薦標籤。" + reserved_tags: + label: Reserved tags + text: "Reserved tags can only be used by moderator." + image_size: + label: Max image size (MB) + text: "The maximum image upload size." + attachment_size: + label: Max attachment size (MB) + text: "The maximum attachment files upload size." + image_megapixels: + label: Max image megapixels + text: "Maximum number of megapixels allowed for an image." + image_extensions: + label: Authorized image extensions + text: "A list of file extensions allowed for image display, separate with commas." + attachment_extensions: + label: Authorized attachment extensions + text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues." + seo: + page_title: 搜尋引擎優化 + permalink: + label: 固定連結 + text: 自定義URL結構可以提高可用性,以及你的連結的向前相容性。 + robots: + label: robots.txt + text: 這將永久覆蓋任何相關的網站設置。 + themes: + page_title: 主題 + themes: + label: 主題 + text: 選擇一個現有主題。 + color_scheme: + label: Color scheme + navbar_style: + label: Navbar background style + primary_color: + label: 主色調 + text: 修改您主題使用的顏色 + css_and_html: + page_title: CSS 與 HTML + custom_css: + label: 自定義CSS + text: > + + head: + label: 頭部 + text: > + + header: + label: 標題 + text: > + + footer: + label: 頁尾 + text: This will insert before </body>. + sidebar: + label: Sidebar + text: This will insert in sidebar. + login: + page_title: 登入 + membership: + title: 會員 + label: 允許新註冊 + text: 關閉以防止任何人創建新帳戶。 + email_registration: + title: Email registration + label: Allow email registration + text: Turn off to prevent anyone creating new account through email. + allowed_email_domains: + title: Allowed email domains + text: Email domains that users must register accounts with. One domain per line. Ignored when empty. + private: + title: 非公開的 + label: 需要登入 + text: 只有登入使用者才能訪問這個社群。 + password_login: + title: Password login + label: Allow email and password login + text: "WARNING: If turn off, you may be unable to log in if you have not previously configured other login method." + installed_plugins: + title: Installed Plugins + plugin_link: Plugins extend and expand the functionality. You may find plugins in the <1>Plugin Repository. + filter: + all: All + active: Active + inactive: Inactive + outdated: Outdated + plugins: + label: Plugins + text: Select an existing plugin. + name: Name + version: Version + status: Status + action: Action + deactivate: Deactivate + activate: Activate + settings: Settings + settings_users: + title: Users + avatar: + label: Default avatar + text: For users without a custom avatar of their own. + gravatar_base_url: + label: Gravatar 基礎網址 + text: URL of the Gravatar provider's API base. Ignored when empty. + profile_editable: + title: Profile editable + allow_update_display_name: + label: Allow users to change their display name + allow_update_username: + label: Allow users to change their username + allow_update_avatar: + label: Allow users to change their profile image + allow_update_bio: + label: Allow users to change their about me + allow_update_website: + label: Allow users to change their website + allow_update_location: + label: Allow users to change their location + privilege: + title: Privileges + level: + label: Reputation required level + text: Choose the reputation required for the privileges + msg: + should_be_number: the input should be number + number_larger_1: number should be equal or larger than 1 + badges: + action: Action + active: Active + activate: Activate + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges + form: + optional: (選填) + empty: 不能為空 + invalid: 是無效的 + btn_submit: 儲存 + not_found_props: "所需屬性 {{ key }} 未找到。" + select: Select + page_review: + review: 審核 + proposed: 提案 + question_edit: 問題編輯 + answer_edit: 回答編輯 + tag_edit: '標籤管理: 編輯標籤' + edit_summary: 編輯摘要 + edit_question: 編輯問題 + edit_answer: 編輯回答 + edit_tag: 編輯標籤 + empty: 沒有剩餘的審核任務。 + approve_revision_tip: Do you approve this revision? + approve_flag_tip: Do you approve this flag? + approve_post_tip: Do you approve this post? + approve_user_tip: Do you approve this user? + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user + queued_post: Queued post + queued_user: Queued user + filter_label: Type + reputation: reputation + flag_post_type: Flagged this post as {{ type }}. + flag_user_type: Flagged this user as {{ type }}. + edit_post: Edit post + list_post: List post + unlist_post: Unlist post + timeline: + undeleted: 未刪除的 + deleted: 刪除 + downvote: 反對 + upvote: 贊同 + accept: 採納 + cancelled: 已取消 + commented: '評論:' + rollback: 回滾 + edited: 最後編輯於 + answered: 回答於 + asked: 提問於 + closed: 關閉 + reopened: 重新開啟 + created: 創建於 + pin: pinned + unpin: unpinned + show: listed + hide: unlisted + title: "歷史記錄" + tag_title: "時間線" + show_votes: "顯示投票" + n_or_a: N/A + title_for_question: "時間線" + title_for_answer: "{{ title }} 的 {{ author }} 回答時間線" + title_for_tag: "標籤的時間線" + datetime: 日期時間 + type: 類型 + by: 由 + comment: 評論 + no_data: "我們找不到任何東西。" + users: + title: 用戶 + users_with_the_most_reputation: Users with the highest reputation scores this week + users_with_the_most_vote: Users who voted the most this week + staffs: 我們的社區工作人員 + reputation: 聲望值 + votes: 選票 + prompt: + leave_page: 你確定要離開此頁面? + changes_not_save: 你所做的變更可能不會儲存。 + draft: + discard_confirm: Are you sure you want to discard your draft? + messages: + post_deleted: This post has been deleted. + post_cancel_deleted: This post has been undeleted. + post_pin: This post has been pinned. + post_unpin: This post has been unpinned. + post_hide_list: This post has been hidden from list. + post_show_list: This post has been shown to list. + post_reopen: This post has been reopened. + post_list: This post has been listed. + post_unlist: This post has been unlisted. + post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved. + post_closed: This post has been closed. + answer_deleted: This answer has been deleted. + answer_cancel_deleted: This answer has been undeleted. + change_user_role: This user's role has been changed. + user_inactive: This user is already inactive. + user_normal: This user is already normal. + user_suspended: This user has been suspended. + user_deleted: This user has been deleted. + badge_activated: This badge has been activated. + badge_inactivated: This badge has been inactivated. + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + external_content_warning: External images/media are not displayed. + + diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 97859ac03..4b71b206f 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -1,30 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package conf import ( - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/server" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/router" - "github.com/answerdev/answer/internal/service/service_config" + "bytes" + "os" + "path/filepath" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/server" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/cli" + "github.com/apache/answer/internal/router" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/pkg/writer" + "github.com/segmentfault/pacman/contrib/conf/viper" + "gopkg.in/yaml.v3" ) // AllConfig all config type AllConfig struct { - Debug bool `json:"debug" mapstructure:"debug"` - Data *Data `json:"data" mapstructure:"data"` - Server *Server `json:"server" mapstructure:"server"` - I18n *translator.I18n `json:"i18n" mapstructure:"i18n"` - Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui"` - ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config"` + Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"` + Server *Server `json:"server" mapstructure:"server" yaml:"server"` + Data *Data `json:"data" mapstructure:"data" yaml:"data"` + I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"` + ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"` + Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"` + UI *server.UI `json:"ui" mapstructure:"ui" yaml:"ui"` +} + +type envConfigOverrides struct { + SwaggerHost string + SwaggerAddressPort string + SiteAddr string +} + +func loadEnvs() (envOverrides *envConfigOverrides) { + return &envConfigOverrides{ + SwaggerHost: os.Getenv("SWAGGER_HOST"), + SwaggerAddressPort: os.Getenv("SWAGGER_ADDRESS_PORT"), + SiteAddr: os.Getenv("SITE_ADDR"), + } +} + +type PathIgnore struct { + Users []string `yaml:"users"` } // Server server config type Server struct { - HTTP *server.HTTP `json:"http" mapstructure:"http"` + HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"` } // Data data config type Data struct { - Database *data.Database `json:"database" mapstructure:"database"` - Cache *data.CacheConf `json:"cache" mapstructure:"cache"` + Database *data.Database `json:"database" mapstructure:"database" yaml:"database"` + Cache *data.CacheConf `json:"cache" mapstructure:"cache" yaml:"cache"` +} + +// SetDefault set default config +func (c *AllConfig) SetDefault() { + if c.UI == nil { + c.UI = &server.UI{} + } +} + +func (c *AllConfig) SetEnvironmentOverrides() { + envs := loadEnvs() + if envs.SiteAddr != "" { + c.Server.HTTP.Addr = envs.SiteAddr + } + if envs.SwaggerHost != "" { + c.Swaggerui.Host = envs.SwaggerHost + } + if envs.SwaggerAddressPort != "" { + c.Swaggerui.Address = envs.SwaggerAddressPort + } +} + +// ReadConfig read config +func ReadConfig(configFilePath string) (c *AllConfig, err error) { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(cli.ConfigFileDir, cli.DefaultConfigFileName) + } + c = &AllConfig{} + config, err := viper.NewWithPath(configFilePath) + if err != nil { + return nil, err + } + if err = config.Parse(&c); err != nil { + return nil, err + } + c.SetDefault() + c.SetEnvironmentOverrides() + return c, nil +} + +// RewriteConfig rewrite config file path +func RewriteConfig(configFilePath string, allConfig *AllConfig) error { + buf := bytes.Buffer{} + enc := yaml.NewEncoder(&buf) + defer enc.Close() + enc.SetIndent(2) + if err := enc.Encode(allConfig); err != nil { + return err + } + return writer.ReplaceFile(configFilePath, buf.String()) } diff --git a/internal/base/constant/acticity.go b/internal/base/constant/acticity.go new file mode 100644 index 000000000..6ea196626 --- /dev/null +++ b/internal/base/constant/acticity.go @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +type ActivityTypeKey string + +const ( + ActEdited = "edited" + ActClosed = "closed" + ActVotedDown = "voted_down" + ActVotedUp = "voted_up" + ActVoteDown = "vote_down" + ActVoteUp = "vote_up" + ActUpVote = "upvote" + ActDownVote = "downvote" + ActFollow = "follow" + ActAccepted = "accepted" + ActAccept = "accept" + ActPin = "pin" + ActUnPin = "unpin" + ActShow = "show" + ActHide = "hide" +) + +const ( + ActQuestionAsked ActivityTypeKey = "question.asked" + ActQuestionClosed ActivityTypeKey = "question.closed" + ActQuestionReopened ActivityTypeKey = "question.reopened" + ActQuestionAnswered ActivityTypeKey = "question.answered" + ActQuestionCommented ActivityTypeKey = "question.commented" + ActQuestionAccept ActivityTypeKey = "question.accept" + ActQuestionUpvote ActivityTypeKey = "question.upvote" + ActQuestionDownVote ActivityTypeKey = "question.downvote" + ActQuestionEdited ActivityTypeKey = "question.edited" + ActQuestionRollback ActivityTypeKey = "question.rollback" + ActQuestionDeleted ActivityTypeKey = "question.deleted" + ActQuestionUndeleted ActivityTypeKey = "question.undeleted" + ActQuestionPin ActivityTypeKey = "question.pin" + ActQuestionUnPin ActivityTypeKey = "question.unpin" + ActQuestionHide ActivityTypeKey = "question.hide" + ActQuestionShow ActivityTypeKey = "question.show" +) + +const ( + ActAnswerAnswered ActivityTypeKey = "answer.answered" + ActAnswerCommented ActivityTypeKey = "answer.commented" + ActAnswerAccept ActivityTypeKey = "answer.accept" + ActAnswerUpvote ActivityTypeKey = "answer.upvote" + ActAnswerDownVote ActivityTypeKey = "answer.downvote" + ActAnswerEdited ActivityTypeKey = "answer.edited" + ActAnswerRollback ActivityTypeKey = "answer.rollback" + ActAnswerDeleted ActivityTypeKey = "answer.deleted" + ActAnswerUndeleted ActivityTypeKey = "answer.undeleted" +) + +const ( + ActTagCreated ActivityTypeKey = "tag.created" + ActTagEdited ActivityTypeKey = "tag.edited" + ActTagRollback ActivityTypeKey = "tag.rollback" + ActTagDeleted ActivityTypeKey = "tag.deleted" + ActTagUndeleted ActivityTypeKey = "tag.undeleted" +) diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go new file mode 100644 index 000000000..987798d19 --- /dev/null +++ b/internal/base/constant/cache_key.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +import "time" + +const ( + UserStatusChangedCacheKey = "answer:user:status:" + UserStatusChangedCacheTime = 7 * 24 * time.Hour + UserTokenCacheKey = "answer:user:token:" + UserTokenCacheTime = 7 * 24 * time.Hour + UserVisitTokenCacheKey = "answer:user:visit:" + UserVisitCacheTime = 7 * 24 * 60 * 60 + UserVisitCookiesCacheKey = "visit" + AdminTokenCacheKey = "answer:admin:token:" + AdminTokenCacheTime = 7 * 24 * time.Hour + UserTokenMappingCacheKey = "answer:user-token:mapping:" + UserEmailCodeCacheKey = "answer:user:email-code:" + UserEmailCodeCacheTime = 10 * time.Minute + UserLatestEmailCodeCacheKey = "answer:user-id:email-code:" + SiteInfoCacheKey = "answer:site-info:" + SiteInfoCacheTime = 1 * time.Hour + ConfigID2KEYCacheKeyPrefix = "answer:config:id:" + ConfigKEY2ContentCacheKeyPrefix = "answer:config:key:" + ConfigCacheTime = 1 * time.Hour + ConnectorUserExternalInfoCacheKey = "answer:connector:" + ConnectorUserExternalInfoCacheTime = 10 * time.Minute + SiteMapQuestionCacheKeyPrefix = "answer:sitemap:question:%d" + SiteMapQuestionCacheTime = time.Hour + SitemapMaxSize = 50000 + NewQuestionNotificationLimitCacheKeyPrefix = "answer:new-question-notification-limit:" + NewQuestionNotificationLimitCacheTime = 7 * 24 * time.Hour + NewQuestionNotificationLimitMax = 50 + RateLimitCacheKeyPrefix = "answer:rate-limit:" + RateLimitCacheTime = 5 * time.Minute + RedDotCacheKey = "answer:red-dot:%s:%s" + RedDotCacheTime = 30 * 24 * time.Hour +) diff --git a/internal/base/constant/comment.go b/internal/base/constant/comment.go new file mode 100644 index 000000000..7dda7c1ff --- /dev/null +++ b/internal/base/constant/comment.go @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +import "time" + +const ( + CommentEditDeadline = time.Minute * 5 +) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 9ccd519a7..ae9e64303 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -1,49 +1,71 @@ -package constant - -import "time" +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ -const ( - Default_PageSize = 20 //Default number of pages - UserStatusChangedCacheKey = "answer:user:status:" - UserStatusChangedCacheTime = 7 * 24 * time.Hour - UserTokenCacheKey = "answer:user:token:" - UserTokenCacheTime = 7 * 24 * time.Hour - AdminTokenCacheKey = "answer:admin:token:" - AdminTokenCacheTime = 7 * 24 * time.Hour - AcceptLanguageFlag = "Accept-Language" -) +package constant const ( - QuestionObjectType = "question" - AnswerObjectType = "answer" - TagObjectType = "tag" - UserObjectType = "user" - CollectionObjectType = "collection" - CommentObjectType = "comment" - ReportObjectType = "report" + DefaultPageSize = 20 // Default number of pages + DefaultBulkUser = 5000 ) -// ObjectTypeStrMapping key => value -// object TagID AnswerList -// key equal database's table name var ( - ObjectTypeStrMapping = map[string]int{ - QuestionObjectType: 1, - AnswerObjectType: 2, - TagObjectType: 3, - UserObjectType: 4, - CollectionObjectType: 6, - CommentObjectType: 7, - ReportObjectType: 8, - } - - ObjectTypeNumberMapping = map[int]string{ - 1: QuestionObjectType, - 2: AnswerObjectType, - 3: TagObjectType, - 4: UserObjectType, - 6: CollectionObjectType, - 7: CommentObjectType, - 8: ReportObjectType, - } + Version = "" + Revision = "" + GoVersion = "" ) + +var Timezones = []string{ + // Americas + "America/New_York", + "America/Chicago", + "America/Los_Angeles", + "America/Toronto", + "America/Vancouver", + "America/Mexico_City", + "America/Sao_Paulo", + "America/Buenos_Aires", + + // Europe + "Europe/London", + "Europe/Paris", + "Europe/Berlin", + "Europe/Madrid", + "Europe/Rome", + "Europe/Moscow", + + // Asia + "Asia/Shanghai", + "Asia/Tokyo", + "Asia/Singapore", + "Asia/Dubai", + "Asia/Hong_Kong", + "Asia/Seoul", + "Asia/Bangkok", + "Asia/Kolkata", + + // Pacific + "Australia/Sydney", + "Australia/Melbourne", + "Pacific/Auckland", + + // Africa + "Africa/Cairo", + "Africa/Johannesburg", + "Africa/Lagos", +} diff --git a/internal/base/constant/ctx_flag.go b/internal/base/constant/ctx_flag.go new file mode 100644 index 000000000..2a757fa87 --- /dev/null +++ b/internal/base/constant/ctx_flag.go @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + AcceptLanguageFlag = "Accept-Language" + ShortIDFlag = "Short-ID-Enabled" +) diff --git a/internal/base/constant/email_tpl_key.go b/internal/base/constant/email_tpl_key.go new file mode 100644 index 000000000..2a06783f7 --- /dev/null +++ b/internal/base/constant/email_tpl_key.go @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + EmailTplKeyChangeEmailTitle = "email_tpl.change_email.title" + EmailTplKeyChangeEmailBody = "email_tpl.change_email.body" + + EmailTplKeyNewAnswerTitle = "email_tpl.new_answer.title" + EmailTplKeyNewAnswerBody = "email_tpl.new_answer.body" + + EmailTplKeyNewCommentTitle = "email_tpl.new_comment.title" + EmailTplKeyNewCommentBody = "email_tpl.new_comment.body" + + EmailTplKeyPassResetTitle = "email_tpl.pass_reset.title" + EmailTplKeyPassResetBody = "email_tpl.pass_reset.body" + + EmailTplKeyRegisterTitle = "email_tpl.register.title" + EmailTplKeyRegisterBody = "email_tpl.register.body" + + EmailTplKeyTestTitle = "email_tpl.test.title" + EmailTplKeyTestBody = "email_tpl.test.body" + + EmailTplKeyInvitedAnswerTitle = "email_tpl.invited_you_to_answer.title" + EmailTplKeyInvitedAnswerBody = "email_tpl.invited_you_to_answer.body" + + EmailTplKeyNewQuestionTitle = "email_tpl.new_question.title" + EmailTplKeyNewQuestionBody = "email_tpl.new_question.body" +) diff --git a/internal/base/constant/event.go b/internal/base/constant/event.go new file mode 100644 index 000000000..f7fd8412a --- /dev/null +++ b/internal/base/constant/event.go @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +// EventType event type. It is used to define the type of event. Such as object.action +type EventType string + +// event object +const ( + eventQuestion = "question" + eventAnswer = "answer" + eventComment = "comment" + eventUser = "user" +) + +// event action +const ( + eventCreate = "create" + eventUpdate = "update" + eventDelete = "delete" + eventVote = "vote" + eventAccept = "accept" // only question have the accept event + eventShare = "share" // the object share link has been clicked + eventFlag = "flag" + eventReact = "react" +) + +const ( + EventUserUpdate EventType = eventUser + "." + eventUpdate + EventUserShare EventType = eventUser + "." + eventShare +) + +const ( + EventQuestionCreate EventType = eventQuestion + "." + eventCreate + EventQuestionUpdate EventType = eventQuestion + "." + eventUpdate + EventQuestionDelete EventType = eventQuestion + "." + eventDelete + EventQuestionVote EventType = eventQuestion + "." + eventVote + EventQuestionAccept EventType = eventQuestion + "." + eventAccept + EventQuestionFlag EventType = eventQuestion + "." + eventFlag + EventQuestionReact EventType = eventQuestion + "." + eventReact +) + +const ( + EventAnswerCreate EventType = eventAnswer + "." + eventCreate + EventAnswerUpdate EventType = eventAnswer + "." + eventUpdate + EventAnswerDelete EventType = eventAnswer + "." + eventDelete + EventAnswerVote EventType = eventAnswer + "." + eventVote + EventAnswerFlag EventType = eventAnswer + "." + eventFlag + EventAnswerReact EventType = eventAnswer + "." + eventReact +) + +const ( + EventCommentCreate EventType = eventComment + "." + eventCreate + EventCommentUpdate EventType = eventComment + "." + eventUpdate + EventCommentDelete EventType = eventComment + "." + eventDelete + EventCommentVote EventType = eventComment + "." + eventVote + EventCommentFlag EventType = eventComment + "." + eventFlag +) diff --git a/internal/base/constant/meta.go b/internal/base/constant/meta.go new file mode 100644 index 000000000..820572d7e --- /dev/null +++ b/internal/base/constant/meta.go @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + ReactionTooltipLabel = "reaction.tooltip" +) diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index fe9fbf419..9a7762d8e 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -1,28 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package constant const ( - // UpdateQuestion update question - UpdateQuestion = "notification.action.update_question" - // AnswerTheQuestion answer the question - AnswerTheQuestion = "notification.action.answer_the_question" - // UpdateAnswer update answer - UpdateAnswer = "notification.action.update_answer" - // AdoptAnswer adopt answer - AdoptAnswer = "notification.action.adopt_answer" - // CommentQuestion comment question - CommentQuestion = "notification.action.comment_question" - // CommentAnswer comment answer - CommentAnswer = "notification.action.comment_answer" - // ReplyToYou reply to you - ReplyToYou = "notification.action.reply_to_you" - // MentionYou mention you - MentionYou = "notification.action.mention_you" - // YourQuestionIsClosed your question is closed - YourQuestionIsClosed = "notification.action.your_question_is_closed" - // YourQuestionWasDeleted your question was deleted - YourQuestionWasDeleted = "notification.action.your_question_was_deleted" - // YourAnswerWasDeleted your answer was deleted - YourAnswerWasDeleted = "notification.action.your_answer_was_deleted" - // YourCommentWasDeleted your comment was deleted - YourCommentWasDeleted = "notification.action.your_comment_was_deleted" + // NotificationUpdateQuestion update question + NotificationUpdateQuestion = "notification.action.update_question" + // NotificationAnswerTheQuestion answer the question + NotificationAnswerTheQuestion = "notification.action.answer_the_question" + // NotificationUpVotedTheQuestion up voted the question + NotificationUpVotedTheQuestion = "notification.action.up_voted_question" + // NotificationDownVotedTheQuestion down voted the question + NotificationDownVotedTheQuestion = "notification.action.down_voted_question" + // NotificationUpdateAnswer update answer + NotificationUpdateAnswer = "notification.action.update_answer" + // NotificationAcceptAnswer accept answer + NotificationAcceptAnswer = "notification.action.accept_answer" + // NotificationUpVotedTheAnswer up voted the answer + NotificationUpVotedTheAnswer = "notification.action.up_voted_answer" + // NotificationDownVotedTheAnswer down voted the answer + NotificationDownVotedTheAnswer = "notification.action.down_voted_answer" + // NotificationCommentQuestion comment question + NotificationCommentQuestion = "notification.action.comment_question" + // NotificationCommentAnswer comment answer + NotificationCommentAnswer = "notification.action.comment_answer" + // NotificationUpVotedTheComment up voted the comment + NotificationUpVotedTheComment = "notification.action.up_voted_comment" + // NotificationReplyToYou reply to you + NotificationReplyToYou = "notification.action.reply_to_you" + // NotificationMentionYou mention you + NotificationMentionYou = "notification.action.mention_you" + // NotificationYourQuestionIsClosed your question is closed + NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed" + // NotificationYourQuestionWasDeleted your question was deleted + NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted" + // NotificationYourAnswerWasDeleted your answer was deleted + NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted" + // NotificationYourCommentWasDeleted your comment was deleted + NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" + // NotificationInvitedYouToAnswer invited you to answer + NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" + // NotificationEarnedBadge earned badge + NotificationEarnedBadge = "notification.action.earned_badge" +) + +type NotificationChannelKey string +type NotificationSource string + +const ( + InboxSource NotificationSource = "inbox" + AllNewQuestionSource NotificationSource = "all_new_question" + AllNewQuestionForFollowingTagsSource NotificationSource = "all_new_question_for_following_tags" +) + +const ( + EmailChannel NotificationChannelKey = "email" +) + +const ( + NotificationTypeInbox = "inbox" + NotificationTypeAchievement = "achievement" + NotificationTypeBadgeAchievement = "badge" +) + +var ( + NotificationMsgTypeMapping = map[string]int{ + NotificationUpdateQuestion: 1, + NotificationAnswerTheQuestion: 1, + NotificationUpVotedTheQuestion: 2, + NotificationDownVotedTheQuestion: 2, + NotificationUpdateAnswer: 1, + NotificationAcceptAnswer: 1, + NotificationUpVotedTheAnswer: 2, + NotificationDownVotedTheAnswer: 2, + NotificationCommentQuestion: 1, + NotificationCommentAnswer: 1, + NotificationUpVotedTheComment: 2, + NotificationReplyToYou: 1, + NotificationMentionYou: 1, + NotificationYourQuestionIsClosed: 1, + NotificationYourQuestionWasDeleted: 1, + NotificationYourAnswerWasDeleted: 1, + NotificationYourCommentWasDeleted: 1, + NotificationInvitedYouToAnswer: 3, + } ) diff --git a/internal/base/constant/object_type.go b/internal/base/constant/object_type.go new file mode 100644 index 000000000..e4ac3d20c --- /dev/null +++ b/internal/base/constant/object_type.go @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + QuestionObjectType = "question" + AnswerObjectType = "answer" + TagObjectType = "tag" + UserObjectType = "user" + CollectionObjectType = "collection" + CommentObjectType = "comment" + ReportObjectType = "report" + BadgeObjectType = "badge" + BadgeAwardObjectType = "badge_award" +) + +var ( + ObjectTypeStrMapping = map[string]int{ + QuestionObjectType: 1, + AnswerObjectType: 2, + TagObjectType: 3, + UserObjectType: 4, + CollectionObjectType: 6, + CommentObjectType: 7, + ReportObjectType: 8, + BadgeObjectType: 9, + BadgeAwardObjectType: 10, + } + + ObjectTypeNumberMapping = map[int]string{ + 1: QuestionObjectType, + 2: AnswerObjectType, + 3: TagObjectType, + 4: UserObjectType, + 6: CollectionObjectType, + 7: CommentObjectType, + 8: ReportObjectType, + 9: BadgeObjectType, + 10: BadgeAwardObjectType, + } +) diff --git a/internal/base/constant/plugin_config_key.go b/internal/base/constant/plugin_config_key.go new file mode 100644 index 000000000..1fb317f09 --- /dev/null +++ b/internal/base/constant/plugin_config_key.go @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + PluginStatus = "plugin.status" +) diff --git a/internal/base/constant/privilege.go b/internal/base/constant/privilege.go new file mode 100644 index 000000000..46fe52d14 --- /dev/null +++ b/internal/base/constant/privilege.go @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +import "github.com/apache/answer/internal/base/reason" + +type Privilege struct { + Key string `json:"key"` + Label string `json:"label"` + Value int `validate:"gte=1" json:"value"` +} + +const ( + RankQuestionAddKey = "rank.question.add" + RankQuestionEditKey = "rank.question.edit" + RankQuestionDeleteKey = "rank.question.delete" + RankQuestionVoteUpKey = "rank.question.vote_up" + RankQuestionVoteDownKey = "rank.question.vote_down" + RankAnswerAddKey = "rank.answer.add" + RankAnswerEditKey = "rank.answer.edit" + RankAnswerDeleteKey = "rank.answer.delete" + RankAnswerAcceptKey = "rank.answer.accept" + RankAnswerVoteUpKey = "rank.answer.vote_up" + RankAnswerVoteDownKey = "rank.answer.vote_down" + RankInviteSomeoneToAnswerKey = "rank.answer.invite_someone_to_answer" + RankCommentAddKey = "rank.comment.add" + RankCommentEditKey = "rank.comment.edit" + RankCommentDeleteKey = "rank.comment.delete" + RankReportAddKey = "rank.report.add" + RankTagAddKey = "rank.tag.add" + RankTagEditKey = "rank.tag.edit" + RankTagDeleteKey = "rank.tag.delete" + RankTagSynonymKey = "rank.tag.synonym" + RankLinkUrlLimitKey = "rank.link.url_limit" + RankVoteDetailKey = "rank.vote.detail" + RankCommentVoteUpKey = "rank.comment.vote_up" + RankCommentVoteDownKey = "rank.comment.vote_down" + RankQuestionEditWithoutReviewKey = "rank.question.edit_without_review" + RankAnswerEditWithoutReviewKey = "rank.answer.edit_without_review" + RankTagEditWithoutReviewKey = "rank.tag.edit_without_review" + RankAnswerAuditKey = "rank.answer.audit" + RankQuestionAuditKey = "rank.question.audit" + RankTagAuditKey = "rank.tag.audit" + RankQuestionCloseKey = "rank.question.close" + RankQuestionReopenKey = "rank.question.reopen" + RankTagUseReservedTagKey = "rank.tag.use_reserved_tag" +) + +var ( + RankAllPrivileges = []*Privilege{ + {Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey}, + {Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey}, + {Label: reason.RankCommentAddLabel, Key: RankCommentAddKey}, + {Label: reason.RankReportAddLabel, Key: RankReportAddKey}, + {Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey}, + {Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey}, + {Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey}, + {Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey}, + {Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey}, + {Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey}, + {Label: reason.RankInviteSomeoneToAnswerLabel, Key: RankInviteSomeoneToAnswerKey}, + {Label: reason.RankTagAddLabel, Key: RankTagAddKey}, + {Label: reason.RankTagEditLabel, Key: RankTagEditKey}, + {Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey}, + {Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey}, + {Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey}, + {Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey}, + {Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey}, + {Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey}, + {Label: reason.RankTagAuditLabel, Key: RankTagAuditKey}, + {Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey}, + {Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey}, + } +) diff --git a/internal/base/constant/question.go b/internal/base/constant/question.go new file mode 100644 index 000000000..f19622085 --- /dev/null +++ b/internal/base/constant/question.go @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + DeletedQuestionTitleTrKey = "question.deleted_title" + QuestionsTitleTrKey = "question.questions_title" + TagsListTitleTrKey = "tag.tags_title" + TagHasNoDescription = "tag.no_description" +) diff --git a/internal/base/constant/reason.go b/internal/base/constant/reason.go new file mode 100644 index 000000000..cf29e4d11 --- /dev/null +++ b/internal/base/constant/reason.go @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + ReasonSpam = "reason.spam" + ReasonRudeOrAbusive = "reason.rude_or_abusive" + ReasonSomething = "reason.something" + ReasonADuplicate = "reason.a_duplicate" + ReasonNotAAnswer = "reason.not_a_answer" + ReasonNoLongerNeeded = "reason.no_longer_needed" + ReasonCommunitySpecific = "reason.community_specific" + ReasonNotClarity = "reason.not_clarity" + ReasonNormal = "reason.normal" + ReasonNormalUser = "reason.normal.user" + ReasonClosed = "reason.closed" + ReasonDeleted = "reason.deleted" + ReasonDeletedUser = "reason.deleted.user" + ReasonSuspended = "reason.suspended" + ReasonInactive = "reason.inactive" + ReasonLooksOk = "reason.looks_ok" + ReasonNeedsEdit = "reason.needs_edit" + ReasonNeedsClose = "reason.needs_close" + ReasonNeedsDelete = "reason.needs_delete" +) diff --git a/internal/base/constant/report.go b/internal/base/constant/report.go deleted file mode 100644 index 4bcca92f5..000000000 --- a/internal/base/constant/report.go +++ /dev/null @@ -1,34 +0,0 @@ -package constant - -const ( - ReportSpamName = "report.spam.name" - ReportSpamDescription = "report.spam.description" - ReportRudeName = "report.rude.name" - ReportRudeDescription = "report.rude.description" - ReportDuplicateName = "report.duplicate.name" - ReportDuplicateDescription = "report.duplicate.description" - ReportOtherName = "report.other.name" - ReportOtherDescription = "report.other.description" - ReportNotAnswerName = "report.not_answer.name" - ReportNotAnswerDescription = "report.not_answer.description" - ReportNotNeedName = "report.not_need.name" - ReportNotNeedDescription = "report.not_need.description" - //question close - QuestionCloseDuplicateName = "question.close.duplicate.name" - QuestionCloseDuplicateDescription = "question.close.duplicate.description" - QuestionCloseGuidelineName = "question.close.guideline.name" - QuestionCloseGuidelineDescription = "question.close.guideline.description" - QuestionCloseMultipleName = "question.close.multiple.name" - QuestionCloseMultipleDescription = "question.close.multiple.description" - QuestionCloseOtherName = "question.close.other.name" - QuestionCloseOtherDescription = "question.close.other.description" -) - -const ( - // TODO put this in database - // TODO need reason controller to resolve - QuestionCloseJson = `[{"name":"question.close.duplicate.name","description":"question.close.duplicate.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"question.close.guideline.name","description":"question.close.guideline.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"question.close.multiple.name","description":"question.close.multiple.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"question.close.other.name","description":"question.close.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` - QuestionReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"report.duplicate.name","description":"report.duplicate.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` - AnswerReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"answer","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"answer","type":2,"have_content":false,"content_type":""},{"name":"report.not_answer.name","description":"report.not_answer.description","source":"answer","type":3,"have_content":false,"content_type":""},{"name":"report.other.name","description":"report.other.description","source":"answer","type":4,"have_content":true,"content_type":"textarea"}]` - CommentReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"comment","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"comment","type":2,"have_content":false,"content_type":""},{"name":"report.not_need.name","description":"report.not_need.description","source":"comment","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"comment","type":4,"have_content":true,"content_type":"textarea"}]` -) diff --git a/internal/base/constant/revision.go b/internal/base/constant/revision.go new file mode 100644 index 000000000..87f3feae9 --- /dev/null +++ b/internal/base/constant/revision.go @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +type ReviewingType string + +const ( + QueuedPost ReviewingType = "queued_post" + QueuedUser ReviewingType = "queued_user" + FlaggedPost ReviewingType = "flagged_post" + FlaggedUser ReviewingType = "flagged_user" + SuggestedPostEdit ReviewingType = "suggested_post_edit" +) + +const ( + ReportOperationEditPost = "edit_post" + ReportOperationClosePost = "close_post" + ReportOperationDeletePost = "delete_post" + ReportOperationUnlistPost = "unlist_post" + ReportOperationIgnoreReport = "ignore_report" +) + +const ( + ReviewQueuedPostLabel = "review.queued_post" + ReviewFlaggedPostLabel = "review.flagged_post" + ReviewSuggestedPostEditLabel = "review.suggested_post_edit" +) diff --git a/internal/base/constant/site_info.go b/internal/base/constant/site_info.go new file mode 100644 index 000000000..2d6668347 --- /dev/null +++ b/internal/base/constant/site_info.go @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + DefaultGravatarBaseURL = "https://www.gravatar.com/avatar/" + DefaultAvatar = "system" + AvatarTypeDefault = "default" + AvatarTypeGravatar = "gravatar" + AvatarTypeCustom = "custom" +) + +const ( + // PermalinkQuestionIDAndTitle /questions/10010000000000001/post-title + PermalinkQuestionIDAndTitle = iota + 1 + // PermalinkQuestionID /questions/10010000000000001 + PermalinkQuestionID + // PermalinkQuestionIDAndTitleByShortID /questions/11/post-title + PermalinkQuestionIDAndTitleByShortID + // PermalinkQuestionIDByShortID /questions/11 + PermalinkQuestionIDByShortID +) + +const ( + ColorSchemeDefault = "default" + ColorSchemeLight = "light" + ColorSchemeDark = "dark" + ColorSchemeSystem = "system" +) + +const ( + EmailConfigKey = "email.config" +) + +const ( + DefaultMaxImageMegapixel = 40 * 1000 * 1000 + DefaultMaxImageSize = 4 * 1024 * 1024 + DefaultMaxAttachmentSize = 8 * 1024 * 1024 +) diff --git a/internal/base/constant/site_type.go b/internal/base/constant/site_type.go new file mode 100644 index 000000000..65b487c5d --- /dev/null +++ b/internal/base/constant/site_type.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" + SiteTypeBranding = "branding" + SiteTypeWrite = "write" + SiteTypeLegal = "legal" + SiteTypeSeo = "seo" + SiteTypeLogin = "login" + SiteTypeCustomCssHTML = "css-html" + SiteTypeTheme = "theme" + SiteTypePrivileges = "privileges" + SiteTypeUsers = "users" +) diff --git a/internal/base/constant/upload.go b/internal/base/constant/upload.go new file mode 100644 index 000000000..d9001fded --- /dev/null +++ b/internal/base/constant/upload.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + AvatarSubPath = "avatar" + AvatarThumbSubPath = "avatar_thumb" + PostSubPath = "post" + BrandingSubPath = "branding" + FilesPostSubPath = "files/post" + DeletedSubPath = "deleted" +) diff --git a/internal/base/constant/user.go b/internal/base/constant/user.go new file mode 100644 index 000000000..80774e0df --- /dev/null +++ b/internal/base/constant/user.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +const ( + UserNormal = "normal" + UserSuspended = "suspended" + UserDeleted = "deleted" + UserInactive = "inactive" +) +const ( + EmailStatusAvailable = 1 + EmailStatusToBeVerified = 2 +) + +const ( + DeletePermanentlyUsers = "users" + DeletePermanentlyQuestions = "questions" + DeletePermanentlyAnswers = "answers" +) + +func ConvertUserStatus(status, mailStatus int) string { + switch status { + case 1: + if mailStatus == EmailStatusToBeVerified { + return UserInactive + } + return UserNormal + case 9: + return UserSuspended + case 10: + return UserDeleted + } + return UserNormal +} diff --git a/internal/base/cron/cron.go b/internal/base/cron/cron.go new file mode 100644 index 000000000..1d8008ad6 --- /dev/null +++ b/internal/base/cron/cron.go @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cron + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/file_record" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/user_admin" + "github.com/robfig/cron/v3" + "github.com/segmentfault/pacman/log" +) + +// ScheduledTaskManager scheduled task manager +type ScheduledTaskManager struct { + siteInfoService siteinfo_common.SiteInfoCommonService + questionService *content.QuestionService + fileRecordService *file_record.FileRecordService + userAdminService *user_admin.UserAdminService + serviceConfig *service_config.ServiceConfig +} + +// NewScheduledTaskManager new scheduled task manager +func NewScheduledTaskManager( + siteInfoService siteinfo_common.SiteInfoCommonService, + questionService *content.QuestionService, + fileRecordService *file_record.FileRecordService, + userAdminService *user_admin.UserAdminService, + serviceConfig *service_config.ServiceConfig, +) *ScheduledTaskManager { + manager := &ScheduledTaskManager{ + siteInfoService: siteInfoService, + questionService: questionService, + fileRecordService: fileRecordService, + userAdminService: userAdminService, + serviceConfig: serviceConfig, + } + return manager +} + +func (s *ScheduledTaskManager) Run() { + log.Infof("cron job manager start") + + s.questionService.SitemapCron(context.Background()) + c := cron.New() + _, err := c.AddFunc("0 */1 * * *", func() { + ctx := context.Background() + log.Infof("sitemap cron execution") + s.questionService.SitemapCron(ctx) + }) + if err != nil { + log.Error(err) + } + + _, err = c.AddFunc("0 */1 * * *", func() { + ctx := context.Background() + log.Infof("refresh hottest cron execution") + s.questionService.RefreshHottestCron(ctx) + }) + if err != nil { + log.Error(err) + } + + // Check for expired user suspensions every 10 minutes + _, err = c.AddFunc("*/10 * * * *", func() { + ctx := context.Background() + log.Infof("checking expired user suspensions") + err := s.userAdminService.CheckAndUnsuspendExpiredUsers(ctx) + if err != nil { + log.Errorf("failed to check expired user suspensions: %v", err) + } + }) + if err != nil { + log.Error(err) + } + + if s.serviceConfig.CleanUpUploads { + log.Infof("clean up uploads cron enabled") + + conf := s.serviceConfig + _, err = c.AddFunc(fmt.Sprintf("0 */%d * * *", conf.CleanOrphanUploadsPeriodHours), func() { + log.Infof("clean orphan upload files cron execution") + s.fileRecordService.CleanOrphanUploadFiles(context.Background()) + }) + if err != nil { + log.Error(err) + } + + _, err = c.AddFunc(fmt.Sprintf("0 0 */%d * *", conf.PurgeDeletedFilesPeriodDays), func() { + log.Infof("purge deleted files cron execution") + s.fileRecordService.PurgeDeletedFiles(context.Background()) + }) + if err != nil { + log.Error(err) + } + } + c.Start() +} diff --git a/internal/base/cron/provider.go b/internal/base/cron/provider.go new file mode 100644 index 000000000..8225289d2 --- /dev/null +++ b/internal/base/cron/provider.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cron + +import ( + "github.com/google/wire" +) + +// ProviderSetService is providers. +var ProviderSetService = wire.NewSet( + NewScheduledTaskManager, +) diff --git a/internal/base/data/config.go b/internal/base/data/config.go index 6bc4ec741..17a62c9c2 100644 --- a/internal/base/data/config.go +++ b/internal/base/data/config.go @@ -1,15 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package data // Database database config type Database struct { - Driver string `json:"driver" mapstructure:"driver"` - Connection string `json:"connection" mapstructure:"connection"` - ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time"` - MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn"` - MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn"` + Driver string `json:"driver" mapstructure:"driver" yaml:"driver"` + Connection string `json:"connection" mapstructure:"connection" yaml:"connection"` + ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time" yaml:"conn_max_life_time,omitempty"` + MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn" yaml:"max_open_conn,omitempty"` + MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"` } // CacheConf cache type CacheConf struct { - FilePath string `json:"file_path" mapstructure:"file_path"` + FilePath string `json:"file_path" mapstructure:"file_path" yaml:"file_path"` } diff --git a/internal/base/data/data.go b/internal/base/data/data.go index 48129d1e7..1d24d7184 100644 --- a/internal/base/data/data.go +++ b/internal/base/data/data.go @@ -1,16 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package data import ( + "path/filepath" "time" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/plugin" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/contrib/cache/memory" "github.com/segmentfault/pacman/log" - "xorm.io/core" + _ "modernc.org/sqlite" "xorm.io/xorm" ormlog "xorm.io/xorm/log" + "xorm.io/xorm/names" "xorm.io/xorm/schemas" ) @@ -34,6 +57,15 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) { if dataConf.Driver == "" { dataConf.Driver = string(schemas.MYSQL) } + if dataConf.Driver == string(schemas.SQLITE) { + dataConf.Driver = "sqlite" + dbFileDir := filepath.Dir(dataConf.Connection) + log.Debugf("try to create database directory %s", dbFileDir) + if err := dir.CreateDirIfNotExist(dbFileDir); err != nil { + log.Errorf("create database dir failed: %s", err) + } + dataConf.MaxOpenConn = 1 + } engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection) if err != nil { return nil, err @@ -58,16 +90,31 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) { if dataConf.ConnMaxLifeTime > 0 { engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second) } - engine.SetColumnMapper(core.GonicMapper{}) + engine.SetColumnMapper(names.GonicMapper{}) return engine, nil } // NewCache new cache instance func NewCache(c *CacheConf) (cache.Cache, func(), error) { + var pluginCache plugin.Cache + _ = plugin.CallCache(func(fn plugin.Cache) error { + pluginCache = fn + return nil + }) + if pluginCache != nil { + return pluginCache, func() {}, nil + } + // TODO What cache type should be initialized according to the configuration file memCache := memory.NewCache() if len(c.FilePath) > 0 { + cacheFileDir := filepath.Dir(c.FilePath) + log.Debugf("try to create cache directory %s", cacheFileDir) + err := dir.CreateDirIfNotExist(cacheFileDir) + if err != nil { + log.Errorf("create cache dir failed: %s", err) + } log.Infof("try to load cache file from %s", c.FilePath) if err := memory.Load(memCache, c.FilePath); err != nil { log.Warn(err) diff --git a/internal/base/handler/handler.go b/internal/base/handler/handler.go index 2fde62de7..7670feea7 100644 --- a/internal/base/handler/handler.go +++ b/internal/base/handler/handler.go @@ -1,15 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package handler import ( "errors" - "net/http" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/validator" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/validator" "github.com/gin-gonic/gin" myErrors "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" + "net/http" ) // HandleResponse Handle response body @@ -40,7 +58,6 @@ func HandleResponse(ctx *gin.Context, err error, data interface{}) { respBody.Data = data } ctx.JSON(myErr.Code, respBody) - return } // BindAndCheck bind request and check @@ -53,10 +70,24 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool { return true } - errField, err := validator.GetValidatorByLang(lang.Abbr()).Check(data) + errField, err := validator.GetValidatorByLang(lang).Check(data) if err != nil { HandleResponse(ctx, err, errField) return true } return false } + +// BindAndCheckReturnErr bind request and check +func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*validator.FormErrorField) { + lang := GetLang(ctx) + if err := ctx.ShouldBind(data); err != nil { + log.Errorf("http_handle BindAndCheck fail, %s", err.Error()) + HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil) + ctx.Abort() + return nil + } + + errFields, _ = validator.GetValidatorByLang(lang).Check(data) + return errFields +} diff --git a/internal/base/handler/lang.go b/internal/base/handler/lang.go index a6b2122c4..a676e5bc2 100644 --- a/internal/base/handler/lang.go +++ b/internal/base/handler/lang.go @@ -1,7 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package handler import ( - "github.com/answerdev/answer/internal/base/constant" + "context" + + "github.com/apache/answer/internal/base/constant" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) @@ -9,12 +30,17 @@ import ( // GetLang get language from header func GetLang(ctx *gin.Context) i18n.Language { acceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag) - switch i18n.Language(acceptLanguage) { - case i18n.LanguageChinese: - return i18n.LanguageChinese - case i18n.LanguageEnglish: - return i18n.LanguageEnglish - default: - return i18n.DefaultLang + if len(acceptLanguage) == 0 { + return i18n.DefaultLanguage + } + return i18n.Language(acceptLanguage) +} + +// GetLangByCtx get language from header +func GetLangByCtx(ctx context.Context) i18n.Language { + acceptLanguage, ok := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language) + if ok { + return acceptLanguage } + return i18n.DefaultLanguage } diff --git a/internal/base/handler/response.go b/internal/base/handler/response.go index 876ba7656..827e0b362 100644 --- a/internal/base/handler/response.go +++ b/internal/base/handler/response.go @@ -1,7 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package handler import ( - "github.com/answerdev/answer/internal/base/translator" + "github.com/apache/answer/internal/base/translator" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/i18n" ) @@ -21,7 +40,7 @@ type RespBody struct { // TrMsg translate the reason cause as a message func (r *RespBody) TrMsg(lang i18n.Language) *RespBody { if len(r.Message) == 0 { - r.Message = translator.GlobalTrans.Tr(lang, r.Reason) + r.Message = translator.Tr(lang, r.Reason) } return r } diff --git a/internal/base/handler/short_id.go b/internal/base/handler/short_id.go new file mode 100644 index 000000000..c763bf944 --- /dev/null +++ b/internal/base/handler/short_id.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package handler + +import ( + "context" + + "github.com/apache/answer/internal/base/constant" +) + +// GetEnableShortID get language from header +func GetEnableShortID(ctx context.Context) bool { + flag, ok := ctx.Value(constant.ShortIDFlag).(bool) + if ok { + return flag + } + return false +} diff --git a/internal/base/middleware/accept_language.go b/internal/base/middleware/accept_language.go new file mode 100644 index 000000000..7a8ee391a --- /dev/null +++ b/internal/base/middleware/accept_language.go @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/i18n" + "golang.org/x/text/language" + "strings" +) + +// ExtractAndSetAcceptLanguage extract accept language from header and set to context +func ExtractAndSetAcceptLanguage(ctx *gin.Context) { + // The language of our front-end configuration, like en_US + lang := handler.GetLang(ctx) + tag, _, err := language.ParseAcceptLanguage(string(lang)) + if err != nil || len(tag) == 0 { + ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) + return + } + + acceptLang := strings.ReplaceAll(tag[0].String(), "-", "_") + + for _, option := range translator.LanguageOptions { + if option.Value == acceptLang { + ctx.Set(constant.AcceptLanguageFlag, i18n.Language(acceptLang)) + return + } + } + + // default language + ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) +} diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go index cf9c87301..f21837b60 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -1,32 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package middleware import ( + "net/http" "strings" - "github.com/answerdev/answer/internal/schema" - - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/auth" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/ui" "github.com/gin-gonic/gin" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) -var ( - ctxUuidKey = "ctxUuidKey" -) +var ctxUUIDKey = "ctxUuidKey" // AuthUserMiddleware auth user middleware type AuthUserMiddleware struct { - authService *auth.AuthService + authService *auth.AuthService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } // NewAuthUserMiddleware new auth user middleware -func NewAuthUserMiddleware(authService *auth.AuthService) *AuthUserMiddleware { +func NewAuthUserMiddleware( + authService *auth.AuthService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware { return &AuthUserMiddleware{ - authService: authService, + authService: authService, + siteInfoCommonService: siteInfoCommonService, } } @@ -44,14 +70,70 @@ func (am *AuthUserMiddleware) Auth() gin.HandlerFunc { return } if userInfo != nil { - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) } ctx.Next() } } -// MustAuth auth user info. If the user does not log in, an unauthenticated error is displayed -func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc { +// EjectUserBySiteInfo if admin config the site can access by nologin user, eject user. +func (am *AuthUserMiddleware) EjectUserBySiteInfo() gin.HandlerFunc { + return func(ctx *gin.Context) { + mustLogin := false + siteInfo, _ := am.siteInfoCommonService.GetSiteLogin(ctx) + if siteInfo != nil { + mustLogin = siteInfo.LoginRequired + } + if !mustLogin { + ctx.Next() + return + } + + // If site in private mode, user must login. + userInfo := GetUserInfoFromContext(ctx) + if userInfo == nil { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + // If user is not active, eject user. + if userInfo.EmailStatus != entity.EmailStatusAvailable { + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive}) + ctx.Abort() + return + } + ctx.Next() + } +} + +// MustAuthWithoutAccountAvailable auth user info, any login user can access though user is not active. +func (am *AuthUserMiddleware) MustAuthWithoutAccountAvailable() gin.HandlerFunc { + return func(ctx *gin.Context) { + token := ExtractToken(ctx) + if len(token) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + userInfo, err := am.authService.GetUserCacheInfo(ctx, token) + if err != nil || userInfo == nil { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + if userInfo.UserStatus == entity.UserStatusDeleted { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + ctx.Abort() + return + } + ctx.Set(ctxUUIDKey, userInfo) + ctx.Next() + } +} + +// MustAuthAndAccountAvailable auth user info and check user status, only allow active user access. +func (am *AuthUserMiddleware) MustAuthAndAccountAvailable() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { @@ -82,12 +164,12 @@ func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc { ctx.Abort() return } - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) ctx.Next() } } -func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { +func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc { return func(ctx *gin.Context) { token := ExtractToken(ctx) if len(token) == 0 { @@ -95,9 +177,9 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { ctx.Abort() return } - userInfo, err := am.authService.GetCmsUserCacheInfo(ctx, token) - if err != nil { - handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + userInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token) + if err != nil || userInfo == nil { + handler.HandleResponse(ctx, errors.Forbidden(reason.UnauthorizedError), nil) ctx.Abort() return } @@ -107,28 +189,61 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { ctx.Abort() return } - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) + } + ctx.Next() + } +} + +func (am *AuthUserMiddleware) CheckPrivateMode() gin.HandlerFunc { + return func(ctx *gin.Context) { + resp, err := am.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + ShowIndexPage(ctx) + ctx.Abort() + return + } + if resp.LoginRequired { + ShowIndexPage(ctx) + ctx.Abort() + return } ctx.Next() } } +func ShowIndexPage(ctx *gin.Context) { + ctx.Header("content-type", "text/html;charset=utf-8") + ctx.Header("X-Frame-Options", "DENY") + file, err := ui.Build.ReadFile("build/index.html") + if err != nil { + log.Error(err) + ctx.Status(http.StatusNotFound) + return + } + ctx.String(http.StatusOK, string(file)) +} // GetLoginUserIDFromContext get user id from context func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { - userInfo, exist := ctx.Get(ctxUuidKey) - if !exist { + userInfo := GetUserInfoFromContext(ctx) + if userInfo == nil { return "" } - u, ok := userInfo.(*entity.UserCacheInfo) - if !ok { - return "" + return userInfo.UserID +} + +// GetIsAdminFromContext get user is admin from context +func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) { + userInfo := GetUserInfoFromContext(ctx) + if userInfo == nil { + return false } - return u.UserID + return userInfo.RoleID == role.RoleAdminID } // GetUserInfoFromContext get user info from context func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) { - userInfo, exist := ctx.Get(ctxUuidKey) + userInfo, exist := ctx.Get(ctxUUIDKey) if !exist { return nil } @@ -139,6 +254,21 @@ func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) { return u } +func GetUserIsAdminModerator(ctx *gin.Context) (isAdminModerator bool) { + userInfo, exist := ctx.Get(ctxUUIDKey) + if !exist { + return false + } + u, ok := userInfo.(*entity.UserCacheInfo) + if !ok { + return false + } + if u.RoleID == role.RoleAdminID || u.RoleID == role.RoleModeratorID { + return true + } + return false +} + func GetLoginUserIDInt64FromContext(ctx *gin.Context) (userID int64) { userIDStr := GetLoginUserIDFromContext(ctx) return converter.StringToInt64(userIDStr) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go new file mode 100644 index 000000000..98430638b --- /dev/null +++ b/internal/base/middleware/avatar.go @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "fmt" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/uploader" + "github.com/apache/answer/pkg/converter" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +type AvatarMiddleware struct { + serviceConfig *service_config.ServiceConfig + uploaderService uploader.UploaderService +} + +// NewAvatarMiddleware new auth user middleware +func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, + uploaderService uploader.UploaderService, +) *AvatarMiddleware { + return &AvatarMiddleware{ + serviceConfig: serviceConfig, + uploaderService: uploaderService, + } +} + +func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { + return func(ctx *gin.Context) { + uri := ctx.Request.RequestURI + if strings.Contains(uri, "/uploads/avatar/") { + size := converter.StringToInt(ctx.Query("s")) + uriWithoutQuery, _ := url.Parse(uri) + filename := filepath.Base(uriWithoutQuery.Path) + filePath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, filename) + var err error + if size != 0 { + filePath, err = am.uploaderService.AvatarThumbFile(ctx, filename, size) + if err != nil { + log.Error(err) + ctx.AbortWithStatus(http.StatusNotFound) + return + } + } + avatarFile, err := os.ReadFile(filePath) + if err != nil { + log.Error(err) + ctx.Abort() + return + } + ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), "."))) + _, err = ctx.Writer.Write(avatarFile) + if err != nil { + log.Error(err) + } + ctx.Abort() + return + + } else { + urlInfo, err := url.Parse(uri) + if err != nil { + ctx.Next() + return + } + ext := strings.TrimPrefix(filepath.Ext(urlInfo.Path), ".") + ctx.Header("content-type", fmt.Sprintf("image/%s", ext)) + } + ctx.Next() + } +} diff --git a/internal/base/middleware/header.go b/internal/base/middleware/header.go new file mode 100644 index 000000000..717d2ac08 --- /dev/null +++ b/internal/base/middleware/header.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" +) + +func HeadersByRequestURI() gin.HandlerFunc { + return func(c *gin.Context) { + if strings.HasPrefix(c.Request.RequestURI, "/static/") { + c.Header("cache-control", "public, max-age=31536000") + } + } +} diff --git a/internal/base/middleware/provider.go b/internal/base/middleware/provider.go index a07318d92..d9a2f8f78 100644 --- a/internal/base/middleware/provider.go +++ b/internal/base/middleware/provider.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package middleware import ( @@ -7,4 +26,7 @@ import ( // ProviderSetMiddleware is providers. var ProviderSetMiddleware = wire.NewSet( NewAuthUserMiddleware, + NewAvatarMiddleware, + NewShortIDMiddleware, + NewRateLimitMiddleware, ) diff --git a/internal/base/middleware/rate_limit.go b/internal/base/middleware/rate_limit.go new file mode 100644 index 000000000..29b961757 --- /dev/null +++ b/internal/base/middleware/rate_limit.go @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "encoding/json" + "fmt" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/repo/limit" + "github.com/apache/answer/pkg/encryption" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +type RateLimitMiddleware struct { + limitRepo *limit.LimitRepo +} + +// NewRateLimitMiddleware new rate limit middleware +func NewRateLimitMiddleware(limitRepo *limit.LimitRepo) *RateLimitMiddleware { + return &RateLimitMiddleware{ + limitRepo: limitRepo, + } +} + +// DuplicateRequestRejection detects and rejects duplicate requests +// It only works for the requests that post content. Such as add question, add answer, comment etc. +func (rm *RateLimitMiddleware) DuplicateRequestRejection(ctx *gin.Context, req any) (reject bool, key string) { + userID := GetLoginUserIDFromContext(ctx) + fullPath := ctx.FullPath() + reqJson, _ := json.Marshal(req) + key = encryption.MD5(fmt.Sprintf("%s:%s:%s", userID, fullPath, string(reqJson))) + var err error + reject, err = rm.limitRepo.CheckAndRecord(ctx, key) + if err != nil { + log.Errorf("check and record rate limit error: %s", err.Error()) + return false, key + } + if !reject { + return false, key + } + log.Debugf("duplicate request: [%s] %s", fullPath, string(reqJson)) + handler.HandleResponse(ctx, errors.BadRequest(reason.DuplicateRequestError), nil) + return true, key +} + +// DuplicateRequestClear clear duplicate request record +func (rm *RateLimitMiddleware) DuplicateRequestClear(ctx *gin.Context, key string) { + err := rm.limitRepo.ClearRecord(ctx, key) + if err != nil { + log.Errorf("clear rate limit error: %s", err.Error()) + } +} diff --git a/internal/base/middleware/short_id.go b/internal/base/middleware/short_id.go new file mode 100644 index 000000000..977eddfed --- /dev/null +++ b/internal/base/middleware/short_id.go @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +type ShortIDMiddleware struct { + siteInfoService siteinfo_common.SiteInfoCommonService +} + +func NewShortIDMiddleware(siteInfoService siteinfo_common.SiteInfoCommonService) *ShortIDMiddleware { + return &ShortIDMiddleware{ + siteInfoService: siteInfoService, + } +} + +func (sm *ShortIDMiddleware) SetShortIDFlag() gin.HandlerFunc { + return func(ctx *gin.Context) { + siteSeo, err := sm.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return + } + ctx.Set(constant.ShortIDFlag, siteSeo.IsShortLink()) + } +} diff --git a/internal/base/middleware/user_center_plugin_auth.go b/internal/base/middleware/user_center_plugin_auth.go new file mode 100644 index 000000000..ac9ee78ad --- /dev/null +++ b/internal/base/middleware/user_center_plugin_auth.go @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +// BanAPIForUserCenter ban api for user center +func BanAPIForUserCenter(ctx *gin.Context) { + uc, ok := plugin.GetUserCenter() + if !ok { + return + } + if !uc.Description().EnabledOriginalUserSystem { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + ctx.Abort() + return + } + ctx.Next() +} diff --git a/internal/base/middleware/visit_img_auth.go b/internal/base/middleware/visit_img_auth.go new file mode 100644 index 000000000..33b62172f --- /dev/null +++ b/internal/base/middleware/visit_img_auth.go @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package middleware + +import ( + "net/http" + "os" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/gin-gonic/gin" +) + +// VisitAuth when user visit the site image, check visit token. This only for private mode. +func (am *AuthUserMiddleware) VisitAuth() gin.HandlerFunc { + return func(ctx *gin.Context) { + if len(os.Getenv("SKIP_FILE_ACCESS_VERIFY")) > 0 { + ctx.Next() + return + } + // If visit brand image, no need to check visit token. Because the brand image is public. + if strings.HasPrefix(ctx.Request.URL.Path, "/uploads/branding/") { + ctx.Next() + return + } + + siteLogin, err := am.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + return + } + if !siteLogin.LoginRequired { + ctx.Next() + return + } + + visitToken, err := ctx.Cookie(constant.UserVisitCookiesCacheKey) + if err != nil || len(visitToken) == 0 { + ctx.Abort() + ctx.Redirect(http.StatusFound, "/403") + return + } + + if !am.authService.CheckUserVisitToken(ctx, visitToken) { + ctx.Abort() + ctx.Redirect(http.StatusFound, "/403") + return + } + } +} diff --git a/internal/base/pager/pager.go b/internal/base/pager/pager.go index d3f57d7de..d7a4caa14 100644 --- a/internal/base/pager/pager.go +++ b/internal/base/pager/pager.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package pager import ( diff --git a/internal/base/pager/pagination.go b/internal/base/pager/pagination.go index fecf05e13..36849fed5 100644 --- a/internal/base/pager/pagination.go +++ b/internal/base/pager/pagination.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package pager import ( @@ -43,3 +62,15 @@ func ValPageAndPageSize(page, pageSize int) (int, int) { } return page, pageSize } + +// ValPageOutOfRange validate page out of range +func ValPageOutOfRange(total int64, page, pageSize int) bool { + if total <= 0 { + return false + } + if pageSize <= 0 { + return true + } + totalPages := (total + int64(pageSize) - 1) / int64(pageSize) + return page < 1 || page > int(totalPages) +} diff --git a/internal/base/reason/privilege.go b/internal/base/reason/privilege.go new file mode 100644 index 000000000..1186a2c31 --- /dev/null +++ b/internal/base/reason/privilege.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package reason + +const ( + PrivilegeLevel1Desc = "privilege.level_1.description" + PrivilegeLevel2Desc = "privilege.level_2.description" + PrivilegeLevel3Desc = "privilege.level_3.description" + PrivilegeLevelCustomDesc = "privilege.level_custom.description" + + RankQuestionAddLabel = "privilege.rank_question_add_label" + RankAnswerAddLabel = "privilege.rank_answer_add_label" + RankCommentAddLabel = "privilege.rank_comment_add_label" + RankReportAddLabel = "privilege.rank_report_add_label" + RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label" + RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label" + RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label" + RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label" + RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label" + RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label" + RankInviteSomeoneToAnswerLabel = "privilege.rank_invite_someone_to_answer_label" + RankTagAddLabel = "privilege.rank_tag_add_label" + RankTagEditLabel = "privilege.rank_tag_edit_label" + RankQuestionEditLabel = "privilege.rank_question_edit_label" + RankAnswerEditLabel = "privilege.rank_answer_edit_label" + RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label" + RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label" + RankQuestionAuditLabel = "privilege.rank_question_audit_label" + RankAnswerAuditLabel = "privilege.rank_answer_audit_label" + RankTagAuditLabel = "privilege.rank_tag_audit_label" + RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label" + RankTagSynonymLabel = "privilege.rank_tag_synonym_label" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index fb64cd781..953c316ed 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package reason const ( @@ -11,30 +30,95 @@ const ( UnauthorizedError = "base.unauthorized_error" // DatabaseError database error DatabaseError = "base.database_error" + // ForbiddenError forbidden error + ForbiddenError = "base.forbidden_error" + // DuplicateRequestError duplicate request error + DuplicateRequestError = "base.duplicate_request_error" +) + +const ( + EmailOrPasswordWrong = "error.object.email_or_password_incorrect" + CommentNotFound = "error.comment.not_found" + CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline" + QuestionNotFound = "error.question.not_found" + QuestionCannotDeleted = "error.question.cannot_deleted" + QuestionCannotClose = "error.question.cannot_close" + QuestionCannotUpdate = "error.question.cannot_update" + QuestionAlreadyDeleted = "error.question.already_deleted" + QuestionUnderReview = "error.question.under_review" + QuestionContentCannotEmpty = "error.question.content_cannot_empty" + AnswerNotFound = "error.answer.not_found" + AnswerCannotDeleted = "error.answer.cannot_deleted" + AnswerCannotUpdate = "error.answer.cannot_update" + AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add" + AnswerRestrictAnswer = "error.answer.restrict_answer" + AnswerContentCannotEmpty = "error.answer.content_cannot_empty" + CommentEditWithoutPermission = "error.comment.edit_without_permission" + CommentContentCannotEmpty = "error.comment.content_cannot_empty" + DisallowVote = "error.object.disallow_vote" + DisallowFollow = "error.object.disallow_follow" + DisallowVoteYourSelf = "error.object.disallow_vote_your_self" + CaptchaVerificationFailed = "error.object.captcha_verification_failed" + OldPasswordVerificationFailed = "error.object.old_password_verification_failed" + NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting" + NewObjectAlreadyDeleted = "error.object.already_deleted" + UserNotFound = "error.user.not_found" + UsernameInvalid = "error.user.username_invalid" + UsernameDuplicate = "error.user.username_duplicate" + UserSetAvatar = "error.user.set_avatar" + EmailDuplicate = "error.email.duplicate" + EmailVerifyURLExpired = "error.email.verify_url_expired" + EmailNeedToBeVerified = "error.email.need_to_be_verified" + EmailIllegalDomainError = "error.email.illegal_email_domain_error" + UserSuspended = "error.user.suspended" + ObjectNotFound = "error.object.not_found" + TagNotFound = "error.tag.not_found" + TagNotContainSynonym = "error.tag.not_contain_synonym_tags" + TagCannotUpdate = "error.tag.cannot_update" + TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete" + TagAlreadyExist = "error.tag.already_exist" + RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" + VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition" + NoEnoughRankToOperate = "error.rank.no_enough_rank_to_operate" + ThemeNotFound = "error.theme.not_found" + LangNotFound = "error.lang.not_found" + ReportHandleFailed = "error.report.handle_failed" + ReportNotFound = "error.report.not_found" + ReadConfigFailed = "error.config.read_config_failed" + DatabaseConnectionFailed = "error.database.connection_failed" + InstallCreateTableFailed = "error.database.create_table_failed" + InstallConfigFailed = "error.install.create_config_failed" + SiteInfoConfigNotFound = "error.site_info.config_not_found" + UploadFileSourceUnsupported = "error.upload.source_unsupported" + UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format" + RecommendTagNotExist = "error.tag.recommend_tag_not_found" + RecommendTagEnter = "error.tag.recommend_tag_enter" + RevisionReviewUnderway = "error.revision.review_underway" + RevisionNoPermission = "error.revision.no_permission" + UserCannotUpdateYourRole = "error.user.cannot_update_your_role" + TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself" + NotAllowedRegistration = "error.user.not_allowed_registration" + NotAllowedLoginViaPassword = "error.user.not_allowed_login_via_password" + SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email" + AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password" + AdminCannotEditTheirProfile = "error.admin.cannot_edit_their_profile" + AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status" + UserAccessDenied = "error.user.access_denied" + UserPageAccessDenied = "error.user.page_access_denied" + AddBulkUsersFormatError = "error.user.add_bulk_users_format_error" + AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error" + InvalidURLError = "error.common.invalid_url" + MetaObjectNotFound = "error.meta.object_not_found" + BadgeObjectNotFound = "error.badge.object_not_found" + StatusInvalid = "error.common.status_invalid" + UserStatusInactive = "error.user.status_inactive" + UserStatusSuspendedForever = "error.user.status_suspended_forever" + UserStatusSuspendedUntil = "error.user.status_suspended_until" + UserStatusDeleted = "error.user.status_deleted" ) +// user external login reasons const ( - EmailOrPasswordWrong = "error.user.email_or_password_wrong" - CommentNotFound = "error.comment.not_found" - QuestionNotFound = "error.question.not_found" - AnswerNotFound = "error.answer.not_found" - CommentEditWithoutPermission = "error.comment.edit_without_permission" - DisallowVote = "error.object.disallow_vote" - DisallowFollow = "error.object.disallow_follow" - DisallowVoteYourSelf = "error.object.disallow_vote_your_self" - CaptchaVerificationFailed = "error.object.captcha_verification_failed" - UserNotFound = "error.user.not_found" - UsernameInvalid = "error.user.username_invalid" - UsernameDuplicate = "error.user.username_duplicate" - EmailDuplicate = "error.email.duplicate" - EmailVerifyUrlExpired = "error.email.verify_url_expired" - EmailNeedToBeVerified = "error.email.need_to_be_verified" - UserSuspended = "error.user.suspended" - ObjectNotFound = "error.object.not_found" - TagNotFound = "error.tag.not_found" - RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition" - ThemeNotFound = "error.theme.not_found" - LangNotFound = "error.lang.not_found" - ReportHandleFailed = "error.report.handle_failed" - ReportNotFound = "error.report.not_found" + UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden" + UserExternalLoginMissingUserID = "error.user.external_login_missing_user_id" ) diff --git a/internal/base/server/config.go b/internal/base/server/config.go index 54cfbf9ba..32b1a040d 100644 --- a/internal/base/server/config.go +++ b/internal/base/server/config.go @@ -1,6 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package server // HTTP http config type HTTP struct { Addr string `json:"addr" mapstructure:"addr"` } + +// UI ui config +type UI struct { + BaseURL string `json:"base_url" mapstructure:"base_url" yaml:"base_url"` + APIBaseURL string `json:"api_base_url" mapstructure:"api_base_url" yaml:"api_base_url"` +} diff --git a/internal/base/server/http.go b/internal/base/server/http.go index c1eb86a66..088bbc9e4 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -1,9 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package server import ( + "html/template" + "io/fs" + brotli "github.com/anargu/gin-brotli" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/router" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/router" + "github.com/apache/answer/plugin" + "github.com/apache/answer/ui" "github.com/gin-gonic/gin" ) @@ -13,7 +37,13 @@ func NewHTTPServer(debug bool, answerRouter *router.AnswerAPIRouter, swaggerRouter *router.SwaggerRouter, viewRouter *router.UIRouter, - authUserMiddleware *middleware.AuthUserMiddleware) *gin.Engine { + authUserMiddleware *middleware.AuthUserMiddleware, + avatarMiddleware *middleware.AvatarMiddleware, + shortIDMiddleware *middleware.ShortIDMiddleware, + templateRouter *router.TemplateRouter, + pluginAPIRouter *router.PluginAPIRouter, + uiConf *UI, +) *gin.Engine { if debug { gin.SetMode(gin.DebugMode) @@ -21,28 +51,56 @@ func NewHTTPServer(debug bool, gin.SetMode(gin.ReleaseMode) } r := gin.New() - r.Use(brotli.Brotli(brotli.DefaultCompression)) + r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag()) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) - viewRouter.Register(r) + html, _ := fs.Sub(ui.Template, "template") + htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*")) + r.SetHTMLTemplate(htmlTemplate) + r.Use(middleware.HeadersByRequestURI()) + viewRouter.Register(r, uiConf.BaseURL) rootGroup := r.Group("") swaggerRouter.Register(rootGroup) - staticRouter.RegisterStaticRouter(rootGroup) + static := r.Group(uiConf.APIBaseURL) + static.Use(avatarMiddleware.AvatarThumb(), authUserMiddleware.VisitAuth()) + staticRouter.RegisterStaticRouter(static) + + // The route must be available without logging in + mustUnAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") + answerRouter.RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware, mustUnAuthV1) // register api that no need to login - unAuthV1 := r.Group("/answer/api/v1") - unAuthV1.Use(authUserMiddleware.Auth()) + unAuthV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") + unAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo()) answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1) + // register api that must be authenticated but no need to check account status + authWithoutStatusV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") + authWithoutStatusV1.Use(authUserMiddleware.MustAuthWithoutAccountAvailable()) + answerRouter.RegisterAuthUserWithAnyStatusAnswerAPIRouter(authWithoutStatusV1) + // register api that must be authenticated - authV1 := r.Group("/answer/api/v1") - authV1.Use(authUserMiddleware.MustAuth()) + authV1 := r.Group(uiConf.APIBaseURL + "/answer/api/v1") + authV1.Use(authUserMiddleware.MustAuthAndAccountAvailable()) answerRouter.RegisterAnswerAPIRouter(authV1) - cmsauthV1 := r.Group("/answer/admin/api") - cmsauthV1.Use(authUserMiddleware.CmsAuth()) - answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1) + adminauthV1 := r.Group(uiConf.APIBaseURL + "/answer/admin/api") + adminauthV1.Use(authUserMiddleware.AdminAuth()) + answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1) + + templateRouter.RegisterTemplateRouter(rootGroup, uiConf.BaseURL) + + // plugin routes + pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1) + pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1) + pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1) + _ = plugin.CallAgent(func(agent plugin.Agent) error { + agent.RegisterUnAuthRouter(mustUnAuthV1) + agent.RegisterAuthUserRouter(authV1) + agent.RegisterAuthAdminRouter(adminauthV1) + return nil + }) return r } diff --git a/internal/base/server/http_funcmap.go b/internal/base/server/http_funcmap.go new file mode 100644 index 000000000..9d8e98f96 --- /dev/null +++ b/internal/base/server/http_funcmap.go @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package server + +import ( + "html/template" + "math" + "regexp" + "strconv" + "strings" + "time" + + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/controller" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/day" + "github.com/apache/answer/pkg/htmltext" + "github.com/segmentfault/pacman/i18n" +) + +var funcMap = template.FuncMap{ + "replaceHTMLTag": func(src string, tags ...string) string { + p := `(?U)<(\d+)>.+` + + re := regexp.MustCompile(p) + ms := re.FindAllStringSubmatch(src, -1) + for _, mi := range ms { + if mi[1] == mi[2] { + i, err := strconv.Atoi(mi[1]) + if err != nil || len(tags) < i { + break + } + + src = strings.ReplaceAll(src, mi[0], tags[i-1]) + } + } + + return src + }, + "join": func(sep string, elems ...string) string { + return strings.Join(elems, sep) + }, + "templateHTML": func(data string) template.HTML { + return template.HTML(data) + }, + "formatLinkNofollow": func(data string) template.HTML { + return template.HTML(FormatLinkNofollow(data)) + }, + "translator": func(la i18n.Language, data string, params ...interface{}) string { + trans := translator.GlobalTrans.Tr(la, data) + + if len(params) > 0 && len(params)%2 == 0 { + for i := 0; i < len(params); i += 2 { + k := converter.InterfaceToString(params[i]) + v := converter.InterfaceToString(params[i+1]) + trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v) + trans = strings.ReplaceAll(trans, "{{"+k+"}}", v) + } + } + + return trans + }, + "timeFormatISO": func(tz string, timestamp int64) string { + _, _ = time.LoadLocation(tz) + return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z") + }, + "translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string { + trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time") + return day.Format(timestamp, trans, tz) + }, + "translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string { + var ( + now = time.Now().Unix() + between int64 = 0 + trans string + ) + _, _ = time.LoadLocation(tz) + if now > timestamp { + between = now - timestamp + } + + if between <= 1 { + return translator.GlobalTrans.Tr(la, "ui.dates.now") + } + + if between > 1 && between < 60 { + trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago") + return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between)) + } + + if between >= 60 && between < 3600 { + min := math.Floor(float64(between / 60)) + trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago") + return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(min, 'f', 0, 64)) + } + + if between >= 3600 && between < 3600*24 { + h := math.Floor(float64(between / 3600)) + trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago") + return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64)) + } + + if between >= 3600*24 && + between < 3600*24*366 && + time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") { + trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date") + return day.Format(timestamp, trans, tz) + } + + trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year") + return day.Format(timestamp, trans, tz) + }, + "wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]interface{} { + return map[string]interface{}{ + "comments": comments, + "language": la, + "timezone": tz, + } + }, + "urlTitle": func(title string) string { + return htmltext.UrlTitle(title) + }, +} + +func FormatLinkNofollow(html string) string { + var hrefRegexp = regexp.MustCompile("(?m).*?") + match := hrefRegexp.FindAllString(html, -1) + for _, v := range match { + hasNofollow := strings.Contains(v, "rel=\"nofollow\"") + hasSiteUrl := strings.Contains(v, controller.SiteUrl) + if !hasSiteUrl { + if !hasNofollow { + nofollowUrl := strings.Replace(v, " 0 + if !res { + field.SetString(trimSpace) + } + return true + case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array: + return field.Len() > 0 + case reflect.Ptr, reflect.Interface, reflect.Func: + return !field.IsNil() + default: + return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface() + } +} + +func Sanitizer(fl validator.FieldLevel) (res bool) { + field := fl.Field() + switch field.Kind() { + case reflect.String: + filter := bluemonday.UGCPolicy() + content := strings.Replace(filter.Sanitize(field.String()), "&", "&", -1) + field.SetString(content) + return true + case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array: + return field.Len() > 0 + case reflect.Ptr, reflect.Interface, reflect.Func: + return !field.IsNil() + default: + return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface() + } +} + func createDefaultValidator(la i18n.Language) *validator.Validate { validate := validator.New() + // _ = validate.RegisterValidation("notblank", validators.NotBlank) + _ = validate.RegisterValidation("notblank", NotBlank) + _ = validate.RegisterValidation("sanitizer", Sanitizer) validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) { defer func() { if len(res) > 0 { - res = translator.GlobalTrans.Tr(la, res) + res = translator.Tr(la, res) } }() if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 { @@ -80,15 +179,34 @@ func createDefaultValidator(la i18n.Language) *validator.Validate { return validate } -func GetValidatorByLang(la string) *MyValidator { - if GlobalValidatorMapping[la] != nil { - return GlobalValidatorMapping[la] +func GetValidatorByLang(lang i18n.Language) *MyValidator { + if GlobalValidatorMapping[lang] != nil { + return GlobalValidatorMapping[lang] } - return GlobalValidatorMapping[i18n.DefaultLang.Abbr()] + return GlobalValidatorMapping[i18n.DefaultLanguage] } // Check / -func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) { +func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err error) { + defer func() { + if len(errFields) == 0 { + return + } + for _, field := range errFields { + if len(field.ErrorField) == 0 { + continue + } + firstRune := []rune(field.ErrorMsg)[0] + if !unicode.IsLetter(firstRune) || !unicode.Is(unicode.Latin, firstRune) { + continue + } + upperFirstRune := unicode.ToUpper(firstRune) + field.ErrorMsg = string(upperFirstRune) + field.ErrorMsg[1:] + if !strings.HasSuffix(field.ErrorMsg, ".") { + field.ErrorMsg += "." + } + } + }() err = m.Validate.Struct(value) if err != nil { var valErrors validator.ValidationErrors @@ -98,24 +216,68 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) } for _, fieldError := range valErrors { - errField = &ErrorField{ - Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()), - Value: fieldError.Translate(m.Tran), + errField := &FormErrorField{ + ErrorField: fieldError.Field(), + ErrorMsg: fieldError.Translate(m.Tran), + } + + // get original tag name from value for set err field key. + structNamespace := fieldError.StructNamespace() + _, fieldName, found := strings.Cut(structNamespace, ".") + if found { + originalTag := getObjectTagByFieldName(value, fieldName) + if len(originalTag) > 0 { + errField.ErrorField = originalTag + } + } + errFields = append(errFields, errField) + } + if len(errFields) > 0 { + errMsg := "" + if len(errFields) == 1 { + errMsg = errFields[0].ErrorMsg } - return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran)) + return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) } } if v, ok := value.(Checker); ok { - errField, err = v.Check() - if err != nil { - return errField, err + errFields, err = v.Check() + if err == nil { + return nil, nil } + errMsg := "" + for _, errField := range errFields { + errField.ErrorMsg = translator.Tr(m.Lang, errField.ErrorMsg) + errMsg = errField.ErrorMsg + } + return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) } return nil, nil } // Checker . type Checker interface { - Check() (errField *ErrorField, err error) + Check() (errField []*FormErrorField, err error) +} + +func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) { + defer func() { + if err := recover(); err != nil { + log.Error(err) + } + }() + + objT := reflect.TypeOf(obj) + objT = objT.Elem() + + structField, exists := objT.FieldByName(fieldName) + if !exists { + return "" + } + tag = structField.Tag.Get("json") + if len(tag) == 0 { + return structField.Tag.Get("form") + } + return tag } diff --git a/internal/cli/build.go b/internal/cli/build.go new file mode 100644 index 000000000..43a7aa9fe --- /dev/null +++ b/internal/cli/build.go @@ -0,0 +1,586 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cli + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "text/template" + + "github.com/Masterminds/semver/v3" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/pkg/writer" + "github.com/apache/answer/ui" + "github.com/segmentfault/pacman/log" + "gopkg.in/yaml.v3" +) + +const ( + mainGoTpl = `package main + +import ( + answercmd "github.com/apache/answer/cmd" + + // remote plugins + {{- range .remote_plugins}} + _ "{{.}}" + {{- end}} + + // local plugins + {{- range .local_plugins}} + _ "answer/{{.}}" + {{- end}} +) + +func main() { + answercmd.Main() +} +` + goModTpl = `module answer + +go 1.23 +` +) + +type answerBuilder struct { + buildingMaterial *buildingMaterial + BuildError error +} + +type buildingMaterial struct { + answerModuleReplacement string + plugins []*pluginInfo + outputPath string + tmpDir string + originalAnswerInfo OriginalAnswerInfo +} + +type OriginalAnswerInfo struct { + Version string + Revision string + Time string +} + +type pluginInfo struct { + // Name of the plugin e.g. github.com/apache/answer-plugins/github-connector + Name string + // Path to the plugin. If path exist, read plugin from local filesystem + Path string + // Version of the plugin + Version string +} + +func newAnswerBuilder(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) *answerBuilder { + material := &buildingMaterial{originalAnswerInfo: originalAnswerInfo} + parentDir, _ := filepath.Abs(".") + if buildDir != "" { + material.tmpDir = filepath.Join(parentDir, buildDir) + } else { + material.tmpDir, _ = os.MkdirTemp(parentDir, "answer_build") + } + if len(outputPath) == 0 { + outputPath = filepath.Join(parentDir, "new_answer") + } + material.outputPath, _ = filepath.Abs(outputPath) + material.plugins = formatPlugins(plugins) + material.answerModuleReplacement = os.Getenv("ANSWER_MODULE") + return &answerBuilder{ + buildingMaterial: material, + } +} + +func (a *answerBuilder) DoTask(task func(b *buildingMaterial) error) { + if a.BuildError != nil { + return + } + a.BuildError = task(a.buildingMaterial) +} + +// BuildNewAnswer builds a new answer with specified plugins +func BuildNewAnswer(buildDir, outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) (err error) { + builder := newAnswerBuilder(buildDir, outputPath, plugins, originalAnswerInfo) + builder.DoTask(createMainGoFile) + builder.DoTask(downloadGoModFile) + builder.DoTask(movePluginToVendor) + builder.DoTask(copyUIFiles) + builder.DoTask(buildUI) + builder.DoTask(mergeI18nFiles) + builder.DoTask(buildBinary) + builder.DoTask(cleanByproduct) + return builder.BuildError +} + +func formatPlugins(plugins []string) (formatted []*pluginInfo) { + for _, plugin := range plugins { + plugin = strings.TrimSpace(plugin) + // plugin description like this 'github.com/apache/answer-plugins/github-connector@latest=/local/path' + info := &pluginInfo{} + plugin, info.Path, _ = strings.Cut(plugin, "=") + info.Name, info.Version, _ = strings.Cut(plugin, "@") + formatted = append(formatted, info) + } + return formatted +} + +// createMainGoFile creates main.go file in tmp dir that content is mainGoTpl +func createMainGoFile(b *buildingMaterial) (err error) { + fmt.Printf("[build] build dir: %s\n", b.tmpDir) + err = dir.CreateDirIfNotExist(b.tmpDir) + if err != nil { + return err + } + + var ( + remotePlugins []string + ) + for _, p := range b.plugins { + remotePlugins = append(remotePlugins, versionedModulePath(p.Name, p.Version)) + } + + mainGoFile := &bytes.Buffer{} + tmpl, err := template.New("main").Parse(mainGoTpl) + if err != nil { + return err + } + err = tmpl.Execute(mainGoFile, map[string]any{ + "remote_plugins": remotePlugins, + }) + if err != nil { + return err + } + + err = writer.WriteFile(filepath.Join(b.tmpDir, "main.go"), mainGoFile.String()) + if err != nil { + return err + } + + err = writer.WriteFile(filepath.Join(b.tmpDir, "go.mod"), goModTpl) + if err != nil { + return err + } + + for _, p := range b.plugins { + // If user set a path, use it to replace the module with local path + if len(p.Path) > 0 { + replacement := fmt.Sprintf("%s@%s=%s", p.Name, p.Version, p.Path) + err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run() + } else if len(p.Version) > 0 { + // If user specify a version, use it to get specific version of the module + err = b.newExecCmd("go", "get", fmt.Sprintf("%s@%s", p.Name, p.Version)).Run() + } + if err != nil { + return err + } + } + return +} + +// downloadGoModFile run go mod commands to download dependencies +func downloadGoModFile(b *buildingMaterial) (err error) { + // If user specify a module replacement, use it. Otherwise, use the latest version. + if len(b.answerModuleReplacement) > 0 { + replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", b.answerModuleReplacement) + err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run() + if err != nil { + return err + } + } + + err = b.newExecCmd("go", "mod", "tidy").Run() + if err != nil { + return err + } + + err = b.newExecCmd("go", "mod", "vendor").Run() + if err != nil { + return err + } + return +} + +// movePluginToVendor move plugin to vendor dir +// Traverse the plugins, and if the plugin path is not github.com/apache/answer-plugins, move the contents of the current plugin to the vendor/github.com/apache/answer-plugins/ directory. +func movePluginToVendor(b *buildingMaterial) (err error) { + pluginsDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer-plugins/") + for _, p := range b.plugins { + pluginDir := filepath.Join(b.tmpDir, "vendor/", p.Name) + pluginName := filepath.Base(p.Name) + if !strings.HasPrefix(p.Name, "github.com/apache/answer-plugins/") { + fmt.Printf("try to copy dir from %s to %s\n", pluginDir, filepath.Join(pluginsDir, pluginName)) + err = copyDirEntries(os.DirFS(pluginDir), ".", filepath.Join(pluginsDir, pluginName), "node_modules") + if err != nil { + return err + } + } + } + return nil +} + +// copyUIFiles copy ui files from answer module to tmp dir +func copyUIFiles(b *buildingMaterial) (err error) { + goListCmd := b.newExecCmd("go", "list", "-mod=mod", "-m", "-f", "{{.Dir}}", "github.com/apache/answer") + buf := new(bytes.Buffer) + goListCmd.Stdout = buf + if err = goListCmd.Run(); err != nil { + return fmt.Errorf("failed to run go list: %w", err) + } + + answerDir := strings.TrimSpace(buf.String()) + goModUIDir := filepath.Join(answerDir, "ui") + localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui/") + // The node_modules folder generated during development will interfere packaging, so it needs to be ignored. + if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir, "node_modules"); err != nil { + return fmt.Errorf("failed to copy ui files: %w", err) + } + + pluginsDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer-plugins/") + localUIPluginDir := filepath.Join(localUIBuildDir, "src/plugins/") + + // copy plugins dir + fmt.Printf("try to copy dir from %s to %s\n", pluginsDir, localUIPluginDir) + + // if plugins dir not exist means no plugins + if !dir.CheckDirExist(pluginsDir) { + return nil + } + + pluginsDirEntries, err := os.ReadDir(pluginsDir) + if err != nil { + return fmt.Errorf("failed to read plugins dir: %w", err) + } + for _, entry := range pluginsDirEntries { + if !entry.IsDir() { + continue + } + sourcePluginDir := filepath.Join(pluginsDir, entry.Name()) + // check if plugin is a ui plugin + packageJsonPath := filepath.Join(sourcePluginDir, "package.json") + fmt.Printf("check if %s is a ui plugin\n", packageJsonPath) + if !dir.CheckFileExist(packageJsonPath) { + continue + } + + pnpmInstallCmd := b.newExecCmd("pnpm", "install") + pnpmInstallCmd.Dir = sourcePluginDir + if err = pnpmInstallCmd.Run(); err != nil { + return fmt.Errorf("failed to install plugin dependencies: %w", err) + } + + localPluginDir := filepath.Join(localUIPluginDir, entry.Name()) + fmt.Printf("try to copy dir from %s to %s\n", sourcePluginDir, localPluginDir) + if err = copyDirEntries(os.DirFS(sourcePluginDir), ".", localPluginDir, "node_modules"); err != nil { + return fmt.Errorf("failed to copy ui files: %w", err) + } + } + formatUIPluginsDirName(localUIPluginDir) + return nil +} + +// overwriteIndexTs overwrites index.ts file in ui/src/plugins/ dir +func overwriteIndexTs(b *buildingMaterial) (err error) { + localUIPluginDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui/src/plugins/") + + folders, err := getFolders(localUIPluginDir) + if err != nil { + return fmt.Errorf("failed to get folders: %w", err) + } + + content := generateIndexTsContent(folders) + err = os.WriteFile(filepath.Join(localUIPluginDir, "index.ts"), []byte(content), 0644) + if err != nil { + return fmt.Errorf("failed to write index.ts: %w", err) + } + return nil +} + +func getFolders(dir string) ([]string, error) { + var folders []string + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, file := range files { + if file.IsDir() && file.Name() != "builtin" { + folders = append(folders, file.Name()) + } + } + return folders, nil +} + +func generateIndexTsContent(folders []string) string { + builder := &strings.Builder{} + builder.WriteString("export default null;\n") + // Line 2:1: Delete `⏎` prettier/prettier + if len(folders) > 0 { + builder.WriteString("\n") + } + for _, folder := range folders { + builder.WriteString(fmt.Sprintf("export { default as %s } from '%s';\n", folder, folder)) + } + return builder.String() +} + +// buildUI run pnpm install and pnpm build commands to build ui +func buildUI(b *buildingMaterial) (err error) { + localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui") + + pnpmInstallCmd := b.newExecCmd("pnpm", "pre-install") + pnpmInstallCmd.Dir = localUIBuildDir + if err = pnpmInstallCmd.Run(); err != nil { + return err + } + + pnpmBuildCmd := b.newExecCmd("pnpm", "build") + pnpmBuildCmd.Dir = localUIBuildDir + if err = pnpmBuildCmd.Run(); err != nil { + return err + } + return nil +} + +func replaceNecessaryFile(b *buildingMaterial) (err error) { + fmt.Printf("try to replace ui build directory\n") + uiBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/ui") + err = copyDirEntries(ui.Build, ".", uiBuildDir) + return err +} + +// mergeI18nFiles merge i18n files +func mergeI18nFiles(b *buildingMaterial) (err error) { + fmt.Printf("try to merge i18n files\n") + + type YamlPluginContent struct { + Plugin map[string]any `yaml:"plugin"` + } + + pluginAllTranslations := make(map[string]*YamlPluginContent) + for _, plugin := range b.plugins { + i18nDir := filepath.Join(b.tmpDir, fmt.Sprintf("vendor/%s/i18n", plugin.Name)) + fmt.Println("i18n dir: ", i18nDir) + if !dir.CheckDirExist(i18nDir) { + continue + } + + entries, err := os.ReadDir(i18nDir) + if err != nil { + return err + } + + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name())) + if err != nil { + log.Debugf("read translation file failed: %s %s", file.Name(), err) + continue + } + + translation := &YamlPluginContent{} + if err = yaml.Unmarshal(buf, translation); err != nil { + log.Debugf("unmarshal translation file failed: %s %s", file.Name(), err) + continue + } + + if pluginAllTranslations[file.Name()] == nil { + pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)} + } + for k, v := range translation.Plugin { + pluginAllTranslations[file.Name()].Plugin[k] = v + } + } + } + + originalI18nDir := filepath.Join(b.tmpDir, "vendor/github.com/apache/answer/i18n") + entries, err := os.ReadDir(originalI18nDir) + if err != nil { + return err + } + + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + filename := file.Name() + if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" { + continue + } + + // if plugin don't have this translation file, ignore it + if pluginAllTranslations[filename] == nil { + continue + } + + out, _ := yaml.Marshal(pluginAllTranslations[filename]) + + buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Debugf("read translation file failed: %s %s", filename, err) + continue + } + + _, _ = buf.WriteString("\n") + _, _ = buf.Write(out) + _ = buf.Close() + } + return err +} + +func copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...string) (err error) { + err = dir.CreateDirIfNotExist(targetDir) + if err != nil { + return err + } + ignoreThisDir := func(path string) bool { + for _, s := range ignoreDir { + if strings.HasPrefix(path, s) { + return true + } + } + return false + } + + err = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if ignoreThisDir(path) { + return nil + } + + // Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes + path = filepath.ToSlash(path) + + // Construct the absolute path for the source file/directory + srcPath := filepath.Join(sourceDir, path) + + // Construct the absolute path for the destination file/directory + dstPath := filepath.Join(targetDir, path) + + if d.IsDir() { + // Create the directory in the destination + err := os.MkdirAll(dstPath, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create directory %s: %w", dstPath, err) + } + } else { + // Open the source file + srcFile, err := sourceFs.Open(srcPath) + if err != nil { + return fmt.Errorf("failed to open source file %s: %w", srcPath, err) + } + defer srcFile.Close() + + // Create the destination file + dstFile, err := os.Create(dstPath) + if err != nil { + return fmt.Errorf("failed to create destination file %s: %w", dstPath, err) + } + defer dstFile.Close() + + // Copy the file contents + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return fmt.Errorf("failed to copy file contents from %s to %s: %w", srcPath, dstPath, err) + } + } + + return nil + }) + + return err +} + +// format plugins dir name from dash to underline +func formatUIPluginsDirName(dirPath string) { + entries, err := os.ReadDir(dirPath) + if err != nil { + fmt.Printf("read ui plugins dir failed: [%s] %s\n", dirPath, err) + return + } + for _, entry := range entries { + if !entry.IsDir() || !strings.Contains(entry.Name(), "-") { + continue + } + newName := strings.ReplaceAll(entry.Name(), "-", "_") + if err := os.Rename(filepath.Join(dirPath, entry.Name()), filepath.Join(dirPath, newName)); err != nil { + fmt.Printf("rename ui plugins dir failed: [%s] %s\n", dirPath, err) + } else { + fmt.Printf("rename ui plugins dir success: [%s] -> [%s]\n", entry.Name(), newName) + } + } +} + +// buildBinary build binary file +func buildBinary(b *buildingMaterial) (err error) { + versionInfo := b.originalAnswerInfo + cmdPkg := "github.com/apache/answer/cmd" + ldflags := fmt.Sprintf("-X %s.Version=%s -X %s.Revision=%s -X %s.Time=%s", + cmdPkg, versionInfo.Version, cmdPkg, versionInfo.Revision, cmdPkg, versionInfo.Time) + err = b.newExecCmd("go", "build", + "-ldflags", ldflags, "-o", b.outputPath, ".").Run() + if err != nil { + return err + } + return +} + +// cleanByproduct delete tmp dir +func cleanByproduct(b *buildingMaterial) (err error) { + return os.RemoveAll(b.tmpDir) +} + +func (b *buildingMaterial) newExecCmd(command string, args ...string) *exec.Cmd { + cmd := exec.Command(command, args...) + fmt.Println(cmd.Args) + cmd.Dir = b.tmpDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd +} + +func versionedModulePath(modulePath, moduleVersion string) string { + if moduleVersion == "" { + return modulePath + } + ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v")) + if err != nil { + return modulePath + } + major := ver.Major() + if major > 1 { + modulePath += fmt.Sprintf("/v%d", major) + } + return path.Clean(modulePath) +} diff --git a/internal/cli/config.go b/internal/cli/config.go new file mode 100644 index 000000000..93cb8eab2 --- /dev/null +++ b/internal/cli/config.go @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cli + +import ( + "context" + "encoding/json" + "fmt" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +type ConfigField struct { + AllowPasswordLogin bool `json:"allow_password_login"` + // The slug name of plugin that you want to deactivate + DeactivatePluginSlugName string `json:"deactivate_plugin_slug_name"` +} + +// SetDefaultConfig set default config +func SetDefaultConfig(dbConf *data.Database, cacheConf *data.CacheConf, field *ConfigField) error { + db, err := data.NewDB(false, dbConf) + if err != nil { + return err + } + defer db.Close() + + cache, cacheCleanup, err := data.NewCache(cacheConf) + if err != nil { + fmt.Println("new cache failed") + } + defer func() { + if cache != nil { + cache.Flush(context.Background()) + cacheCleanup() + } + }() + + if field.AllowPasswordLogin { + return defaultLoginConfig(db) + } + if len(field.DeactivatePluginSlugName) > 0 { + return deactivatePlugin(db, field.DeactivatePluginSlugName) + } + + return nil +} + +func defaultLoginConfig(x *xorm.Engine) (err error) { + fmt.Println("set default login config") + + loginSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeLogin, + } + exist, err := x.Get(loginSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + var content map[string]any + _ = json.Unmarshal([]byte(loginSiteInfo.Content), &content) + content["allow_password_login"] = true + dataByte, _ := json.Marshal(content) + loginSiteInfo.Content = string(dataByte) + _, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + return nil +} + +func deactivatePlugin(x *xorm.Engine, pluginSlugName string) (err error) { + fmt.Printf("try to deactivate plugin: %s\n", pluginSlugName) + + item := &entity.Config{Key: constant.PluginStatus} + exist, err := x.Get(item) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + return nil + } + + pluginStatusMapping := make(map[string]bool) + _ = json.Unmarshal([]byte(item.Value), &pluginStatusMapping) + status, ok := pluginStatusMapping[pluginSlugName] + if !ok { + fmt.Printf("plugin %s not exist\n", pluginSlugName) + return nil + } + if !status { + fmt.Printf("plugin %s already deactivated\n", pluginSlugName) + return nil + } + + pluginStatusMapping[pluginSlugName] = false + dataByte, _ := json.Marshal(pluginStatusMapping) + item.Value = string(dataByte) + _, err = x.ID(item.ID).Cols("value").Update(item) + if err != nil { + return fmt.Errorf("update plugin status failed: %w", err) + } + return nil +} diff --git a/internal/cli/dump.go b/internal/cli/dump.go index 7b84d862e..e5d528212 100644 --- a/internal/cli/dump.go +++ b/internal/cli/dump.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package cli import ( @@ -5,7 +24,7 @@ import ( "path/filepath" "time" - "github.com/answerdev/answer/internal/base/data" + "github.com/apache/answer/internal/base/data" "xorm.io/xorm/schemas" ) @@ -15,6 +34,7 @@ func DumpAllData(dataConf *data.Database, dumpDataPath string) error { if err != nil { return err } + defer db.Close() if err = db.Ping(); err != nil { return err } diff --git a/internal/cli/i18n.go b/internal/cli/i18n.go new file mode 100644 index 000000000..faef0f28a --- /dev/null +++ b/internal/cli/i18n.go @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package cli + +import ( + "fmt" + "github.com/apache/answer/i18n" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/pkg/writer" + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "strings" +) + +type YamlPluginContent struct { + Plugin map[string]any `yaml:"plugin"` +} + +// ReplaceI18nFilesLocal replace i18n files +func ReplaceI18nFilesLocal(i18nDir string) error { + i18nList, err := i18n.I18n.ReadDir(".") + if err != nil { + fmt.Println(err.Error()) + return err + } + fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList)) + for _, item := range i18nList { + path := filepath.Join(i18nDir, item.Name()) + content, err := i18n.I18n.ReadFile(item.Name()) + if err != nil { + continue + } + exist := dir.CheckFileExist(path) + if exist { + fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name()) + if err = os.Remove(path); err != nil { + fmt.Println(err) + } + } + fmt.Printf("[i18n] install %s bundle...\n", item.Name()) + err = writer.WriteFile(path, string(content)) + if err != nil { + fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) + } else { + fmt.Printf("[i18n] install %s bundle success\n", item.Name()) + } + } + return nil +} + +// MergeI18nFilesLocal merge i18n files +func MergeI18nFilesLocal(originalI18nDir, targetI18nDir string) (err error) { + pluginAllTranslations := make(map[string]*YamlPluginContent) + + err = findI18nFileInDir(pluginAllTranslations, targetI18nDir) + if err != nil { + return err + } + + entries, err := os.ReadDir(originalI18nDir) + if err != nil { + return err + } + + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + filename := file.Name() + if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" { + continue + } + + // if plugin don't have this translation file, ignore it + if pluginAllTranslations[filename] == nil { + continue + } + + out, _ := yaml.Marshal(pluginAllTranslations[filename]) + + buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("[i18n] read translation file failed: %s %s\n", filename, err) + continue + } + + _, _ = buf.WriteString("\n") + _, _ = buf.Write(out) + _ = buf.Close() + fmt.Printf("[i18n] merge i18n file: %s success\n", filename) + } + + return nil +} + +// find i18n file in dir +func findI18nFileInDir(pluginAllTranslations map[string]*YamlPluginContent, i18nDir string) error { + // if i18n dir is not i18n, find deeper + dirBase := filepath.Base(i18nDir) + if dirBase != "i18n" { + if strings.HasPrefix(dirBase, ".") { + return nil + } + // find all i18n dir in target dir + targetDirs, err := os.ReadDir(i18nDir) + if err != nil { + return err + } + + for _, targetDir := range targetDirs { + if targetDir.IsDir() { + if err := findI18nFileInDir(pluginAllTranslations, filepath.Join(i18nDir, targetDir.Name())); err != nil { + fmt.Printf("[i18n] find i18n file in dir failed: %s %s\n", targetDir.Name(), err) + } + } + } + return nil + } + + fmt.Printf("[i18n] find i18n file in dir: %s\n", i18nDir) + + // if i18nDir is i18n, find all yaml files + entries, err := os.ReadDir(i18nDir) + if err != nil { + return err + } + + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name())) + if err != nil { + fmt.Printf("[i18n] read translation file failed: %s %s\n", file.Name(), err) + continue + } + + translation := &YamlPluginContent{} + if err = yaml.Unmarshal(buf, translation); err != nil { + fmt.Printf("[i18n] unmarshal translation file failed: %s %s\n", file.Name(), err) + continue + } + + if pluginAllTranslations[file.Name()] == nil { + pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)} + } + for k, v := range translation.Plugin { + pluginAllTranslations[file.Name()].Plugin[k] = v + } + } + return nil +} diff --git a/internal/cli/install.go b/internal/cli/install.go index 34da2e5a1..69e673d57 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -1,60 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package cli import ( - "bufio" "fmt" "os" "path/filepath" + "sync" - "github.com/answerdev/answer/configs" - "github.com/answerdev/answer/i18n" - "github.com/answerdev/answer/pkg/dir" + "github.com/apache/answer/configs" + "github.com/apache/answer/i18n" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/pkg/writer" ) const ( - DefaultConfigFileName = "config.yaml" + DefaultConfigFileName = "config.yaml" + DefaultCacheFileName = "cache.db" + DefaultReservedUsernamesConfigFileName = "reserved-usernames.json" ) var ( - ConfigFilePath = "/conf/" - UploadFilePath = "/upfiles/" - I18nPath = "/i18n/" + ConfigFileDir = "/conf/" + UploadFilePath = "/uploads/" + I18nPath = "/i18n/" + CacheDir = "/cache/" + formatAllPathONCE sync.Once ) +// GetConfigFilePath get config file path +func GetConfigFilePath() string { + return filepath.Join(ConfigFileDir, DefaultConfigFileName) +} + +func FormatAllPath(dataDirPath string) { + formatAllPathONCE.Do(func() { + ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir) + UploadFilePath = filepath.Join(dataDirPath, UploadFilePath) + I18nPath = filepath.Join(dataDirPath, I18nPath) + CacheDir = filepath.Join(dataDirPath, CacheDir) + }) +} + // InstallAllInitialEnvironment install all initial environment func InstallAllInitialEnvironment(dataDirPath string) { - ConfigFilePath = filepath.Join(dataDirPath, ConfigFilePath) - UploadFilePath = filepath.Join(dataDirPath, UploadFilePath) - I18nPath = filepath.Join(dataDirPath, I18nPath) - - installConfigFile() + FormatAllPath(dataDirPath) installUploadDir() - installI18nBundle() + InstallI18nBundle(false) fmt.Println("install all initial environment done") - return } -func installConfigFile() { - fmt.Println("[config-file] try to install...") - defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName) +func InstallConfigFile(configFilePath string) error { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName) + } + fmt.Println("[config-file] try to create at ", configFilePath) // if config file already exists do nothing. - if CheckConfigFile(defaultConfigFile) { - fmt.Printf("[config-file] %s already exists\n", defaultConfigFile) - return + if CheckConfigFile(configFilePath) { + fmt.Printf("[config-file] %s already exists\n", configFilePath) + return nil } - if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil { + if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil { fmt.Printf("[config-file] create directory fail %s\n", err.Error()) - return + return fmt.Errorf("create directory fail %s", err.Error()) } - fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile) + fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) - if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil { + if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) - return + return fmt.Errorf("write file failed %s", err) } fmt.Printf("[config-file] install success\n") + return nil } func installUploadDir() { @@ -66,8 +102,12 @@ func installUploadDir() { } } -func installI18nBundle() { +func InstallI18nBundle(replace bool) { fmt.Println("[i18n] try to install i18n bundle...") + // if SKIP_REPLACE_I18N is set, skip replace i18n bundles + if len(os.Getenv("SKIP_REPLACE_I18N")) > 0 { + replace = false + } if err := dir.CreateDirIfNotExist(I18nPath); err != nil { fmt.Println(err.Error()) return @@ -85,8 +125,18 @@ func installI18nBundle() { if err != nil { continue } + exist := dir.CheckFileExist(path) + if exist && !replace { + continue + } + if exist { + fmt.Printf("[i18n] install %s file exist, try to replace it\n", item.Name()) + if err = os.Remove(path); err != nil { + fmt.Println(err) + } + } fmt.Printf("[i18n] install %s bundle...\n", item.Name()) - err = writerFile(path, string(content)) + err = writer.WriteFile(path, string(content)) if err != nil { fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) } else { @@ -94,21 +144,3 @@ func installI18nBundle() { } } } - -func writerFile(filePath, content string) error { - file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return err - } - defer func() { - _ = file.Close() - }() - writer := bufio.NewWriter(file) - if _, err := writer.WriteString(content); err != nil { - return err - } - if err := writer.Flush(); err != nil { - return err - } - return nil -} diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go index fb05e33f4..75ed5d3b2 100644 --- a/internal/cli/install_check.go +++ b/internal/cli/install_check.go @@ -1,8 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package cli import ( - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/pkg/dir" + "fmt" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/dir" ) func CheckConfigFile(configPath string) bool { @@ -13,12 +35,42 @@ func CheckUploadDir() bool { return dir.CheckDirExist(UploadFilePath) } -func CheckDB(dataConf *data.Database) bool { +// CheckDBConnection check database whether the connection is normal +func CheckDBConnection(dataConf *data.Database) bool { db, err := data.NewDB(false, dataConf) if err != nil { + fmt.Printf("connection database failed: %s\n", err) return false } + defer db.Close() if err = db.Ping(); err != nil { + fmt.Printf("connection ping database failed: %s\n", err) + return false + } + + return true +} + +// CheckDBTableExist check database whether the table is already exists +func CheckDBTableExist(dataConf *data.Database) bool { + db, err := data.NewDB(false, dataConf) + if err != nil { + fmt.Printf("connection database failed: %s\n", err) + return false + } + defer db.Close() + if err = db.Ping(); err != nil { + fmt.Printf("connection ping database failed: %s\n", err) + return false + } + + exist, err := db.IsTableExist(&entity.Version{}) + if err != nil { + fmt.Printf("check table exist failed: %s\n", err) + return false + } + if !exist { + fmt.Printf("check table not exist\n") return false } return true diff --git a/internal/controller/activity_controller.go b/internal/controller/activity_controller.go new file mode 100644 index 000000000..26cd400c7 --- /dev/null +++ b/internal/controller/activity_controller.go @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/pkg/uid" + "github.com/gin-gonic/gin" +) + +type ActivityController struct { + activityService *activity.ActivityService +} + +// NewActivityController new activity controller. +func NewActivityController( + activityService *activity.ActivityService) *ActivityController { + return &ActivityController{activityService: activityService} +} + +// GetObjectTimeline get object timeline +// @Summary get object timeline +// @Description get object timeline +// @Tags Comment +// @Produce json +// @Param object_id query string false "object id" +// @Param tag_slug_name query string false "tag slug name" +// @Param object_type query string false "object type" Enums(question, answer, tag) +// @Param show_vote query boolean false "is show vote" +// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} +// @Router /answer/api/v1/activity/timeline [get] +func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) { + req := &schema.GetObjectTimelineReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.ObjectID = uid.DeShortID(req.ObjectID) + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil { + req.IsAdmin = userInfo.RoleID == role.RoleAdminID + } + + resp, err := ac.activityService.GetObjectTimeline(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// GetObjectTimelineDetail get object timeline detail +// @Summary get object timeline detail +// @Description get object timeline detail +// @Tags Comment +// @Produce json +// @Param revision_id query string true "revision id" +// @Success 200 {object} handler.RespBody{data=schema.GetObjectTimelineResp} +// @Router /answer/api/v1/activity/timeline/detail [get] +func (ac *ActivityController) GetObjectTimelineDetail(ctx *gin.Context) { + req := &schema.GetObjectTimelineDetailReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := ac.activityService.GetObjectTimelineDetail(ctx, req) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 1223b0377..7c5aca1db 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -1,34 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( "fmt" + "net/http" - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/rank" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService - rankService *rank.RankService + answerService *content.AnswerService + rankService *rank.RankService + actionService *action.CaptchaService + siteInfoCommonService siteinfo_common.SiteInfoCommonService + rateLimitMiddleware *middleware.RateLimitMiddleware } // NewAnswerController new controller -func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController { - return &AnswerController{answerService: answerService, rankService: rankService} +func NewAnswerController( + answerService *content.AnswerService, + rankService *rank.RankService, + actionService *action.CaptchaService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + rateLimitMiddleware *middleware.RateLimitMiddleware, +) *AnswerController { + return &AnswerController{ + answerService: answerService, + rankService: rankService, + actionService: actionService, + siteInfoCommonService: siteInfoCommonService, + rateLimitMiddleware: rateLimitMiddleware, + } } // RemoveAnswer delete answer // @Summary delete answer // @Description delete answer -// @Tags api-answer +// @Tags Answer // @Accept json // @Produce json // @Security ApiKeyAuth @@ -40,31 +81,91 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } + req.ID = uid.DeShortID(req.ID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanDelete = canList[0] || objectOwner + if !req.CanDelete { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = ac.answerService.RemoveAnswer(ctx, req) + if !isAdmin { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } + handler.HandleResponse(ctx, err, nil) +} +// RecoverAnswer recover answer +// @Summary recover answer +// @Description recover the deleted answer +// @Tags Answer +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.RecoverAnswerReq true "answer" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/answer/recover [post] +func (ac *AnswerController) RecoverAnswer(ctx *gin.Context) { + req := &schema.RecoverAnswerReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.AnswerID = uid.DeShortID(req.AnswerID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerUnDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := ac.answerService.RemoveAnswer(ctx, req.ID) + err = ac.answerService.RecoverAnswer(ctx, req) handler.HandleResponse(ctx, err, nil) } -// Get godoc -// @Summary Get Answer -// @Description Get Answer -// @Tags api-answer -// @Accept json -// @Produce json -// @Param id query string true "Answer TagID" default(1) -// @Router /answer/api/v1/answer/info [get] -// @Success 200 {string} string "" -func (ac *AnswerController) Get(ctx *gin.Context) { +// GetAnswerInfo get answer info +// @Summary Get Answer Detail +// @Description Get Answer Detail +// @Tags Answer +// @Accept json +// @Produce json +// @Param id query string true "id" +// @Success 200 {object} handler.RespBody{data=schema.GetAnswerInfoResp} +// @Router /answer/api/v1/answer/info [get] +func (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) { id := ctx.Query("id") - userId := middleware.GetLoginUserIDFromContext(ctx) + id = uid.DeShortID(id) + userID := middleware.GetLoginUserIDFromContext(ctx) - info, questionInfo, has, err := ac.answerService.Get(ctx, id, userId) + info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID) if err != nil { handler.HandleResponse(ctx, err, gin.H{}) return @@ -73,115 +174,223 @@ func (ac *AnswerController) Get(ctx *gin.Context) { handler.HandleResponse(ctx, fmt.Errorf(""), gin.H{}) return } - handler.HandleResponse(ctx, err, gin.H{ - "info": info, - "question": questionInfo, + handler.HandleResponse(ctx, err, &schema.GetAnswerInfoResp{ + Info: info, + Question: questionInfo, }) } -// Add godoc -// @Summary Insert Answer -// @Description Insert Answer -// @Tags api-answer -// @Accept json -// @Produce json +// AddAnswer add answer +// @Summary Add Answer +// @Description add answer +// @Tags Answer +// @Accept json +// @Produce json // @Security ApiKeyAuth -// @Param data body schema.AnswerAddReq true "AnswerAddReq" -// @Success 200 {string} string "" +// @Param data body schema.AnswerAddReq true "add answer request" +// @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer [post] -func (ac *AnswerController) Add(ctx *gin.Context) { +func (ac *AnswerController) AddAnswer(ctx *gin.Context) { req := &schema.AnswerAddReq{} if handler.BindAndCheck(ctx, req) { return } + reject, rejectKey := ac.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) + if reject { + return + } + defer func() { + // If status is not 200 means that the bad request has been returned, so the record should be cleared + if ctx.Writer.Status() != http.StatusOK { + ac.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) + } + }() + req.QuestionID = uid.DeShortID(req.QuestionID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerEdit, + permission.AnswerDelete, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + linkUrlLimitUser := canList[2] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + write, err := ac.siteInfoCommonService.GetSiteWrite(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) return } + if write.RestrictAnswer { + // check if there's already an answer by this user + ids, err := ac.answerService.GetCountByUserIDQuestionID(ctx, req.UserID, req.QuestionID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if len(ids) >= 1 { + handler.HandleResponse(ctx, errors.Forbidden(reason.AnswerRestrictAnswer), nil) + return + } + } + + req.UserAgent = ctx.GetHeader("User-Agent") + req.IP = ctx.ClientIP() - answerId, err := ac.answerService.Insert(ctx, req) + answerID, err := ac.answerService.Insert(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } - info, questionInfo, has, err := ac.answerService.Get(ctx, answerId, req.UserID) + if !isAdmin || !linkUrlLimitUser { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID) + } + info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !has { - //todo !has handler.HandleResponse(ctx, nil, nil) return } + + objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, info.ID) + req.CanEdit = canList[0] || objectOwner + req.CanDelete = canList[1] || objectOwner + info.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, info.UserID, + 0, req.CanEdit, req.CanDelete, false) handler.HandleResponse(ctx, nil, gin.H{ "info": info, "question": questionInfo, }) - } -// Update godoc +// UpdateAnswer update answer // @Summary Update Answer // @Description Update Answer -// @Tags api-answer -// @Accept json -// @Produce json +// @Tags Answer +// @Accept json +// @Produce json // @Security ApiKeyAuth -// @Param data body schema.AnswerUpdateReq true "AnswerUpdateReq" -// @Success 200 {string} string "" +// @Param data body schema.AnswerUpdateReq true "AnswerUpdateReq" +// @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer [put] -func (ac *AnswerController) Update(ctx *gin.Context) { +func (ac *AnswerController) UpdateAnswer(ctx *gin.Context) { req := &schema.AnswerUpdateReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerEdit, + permission.AnswerEditWithoutReview, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.QuestionID = uid.DeShortID(req.QuestionID) + linkUrlLimitUser := canList[2] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := ac.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) + req.CanEdit = canList[0] || objectOwner + req.NoNeedReview = canList[1] || objectOwner + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - _, err := ac.answerService.Update(ctx, req) + _, err = ac.answerService.Update(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } - info, questionInfo, has, err := ac.answerService.Get(ctx, req.ID, req.UserID) + if !isAdmin || !linkUrlLimitUser { + ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } + _, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } - if !has { - //todo !has - handler.HandleResponse(ctx, nil, nil) - return - } - handler.HandleResponse(ctx, nil, gin.H{ - "info": info, - "question": questionInfo, - }) + handler.HandleResponse(ctx, nil, &schema.AnswerUpdateResp{WaitForReview: !req.NoNeedReview}) } // AnswerList godoc // @Summary AnswerList // @Description AnswerList
order (default or updated) -// @Tags api-answer -// @Security ApiKeyAuth -// @Accept json -// @Produce json -// @Param data body schema.AnswerList true "AnswerList" +// @Tags Answer +// @Accept json +// @Produce json +// @Param question_id query string true "question_id" +// @Param order query string true "order" +// @Param page query string true "page" +// @Param page_size query string true "page_size" // @Success 200 {string} string "" -// @Router /answer/api/v1/answer/list [get] +// @Router /answer/api/v1/answer/page [get] func (ac *AnswerController) AnswerList(ctx *gin.Context) { - req := &schema.AnswerList{} + req := &schema.AnswerListReq{} if handler.BindAndCheck(ctx, req) { return } - req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.QuestionID = uid.DeShortID(req.QuestionID) + + canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerEdit, + permission.AnswerDelete, + permission.AnswerUnDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] + req.CanRecover = canList[2] + list, count, err := ac.answerService.SearchList(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) @@ -193,47 +402,57 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) { }) } -// Adopted godoc -// @Summary Adopted -// @Description Adopted -// @Tags api-answer -// @Accept json -// @Produce json +// AcceptAnswer accept answer +// @Summary Accept Answer +// @Description Accept Answer +// @Tags Answer +// @Accept json +// @Produce json // @Security ApiKeyAuth -// @Param data body schema.AnswerAdoptedReq true "AnswerAdoptedReq" -// @Success 200 {string} string "" +// @Param data body schema.AcceptAnswerReq true "AcceptAnswerReq" +// @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/answer/acceptance [post] -func (ac *AnswerController) Adopted(ctx *gin.Context) { - req := &schema.AnswerAdoptedReq{} +func (ac *AnswerController) AcceptAnswer(ctx *gin.Context) { + req := &schema.AcceptAnswerReq{} if handler.BindAndCheck(ctx, req) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAcceptRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + req.AnswerID = uid.DeShortID(req.AnswerID) + req.QuestionID = uid.DeShortID(req.QuestionID) + can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := ac.answerService.UpdateAdopted(ctx, req) + err = ac.answerService.AcceptAnswer(ctx, req) handler.HandleResponse(ctx, err, nil) } -// AdminSetAnswerStatus godoc -// @Summary AdminSetAnswerStatus -// @Description Status:[available,deleted] +// AdminUpdateAnswerStatus update answer status +// @Summary update answer status +// @Description update answer status // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body entity.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest" -// @Router /answer/admin/api/answer/status [put] +// @Param data body schema.AdminUpdateAnswerStatusReq true "AdminUpdateAnswerStatusReq" // @Success 200 {object} handler.RespBody -func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) { - req := &entity.AdminSetAnswerStatusRequest{} +// @Router /answer/admin/api/answer/status [put] +func (ac *AnswerController) AdminUpdateAnswerStatus(ctx *gin.Context) { + req := &schema.AdminUpdateAnswerStatusReq{} if handler.BindAndCheck(ctx, req) { return } - err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr) - handler.HandleResponse(ctx, err, gin.H{}) + req.AnswerID = uid.DeShortID(req.AnswerID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + err := ac.answerService.AdminSetAnswerStatus(ctx, req) + handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go new file mode 100644 index 000000000..61197e715 --- /dev/null +++ b/internal/controller/badge_controller.go @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/pkg/uid" + "github.com/gin-gonic/gin" +) + +type BadgeController struct { + badgeService *badge.BadgeService + badgeAwardService *badge.BadgeAwardService +} + +func NewBadgeController( + badgeService *badge.BadgeService, + badgeAwardService *badge.BadgeAwardService) *BadgeController { + return &BadgeController{ + badgeService: badgeService, + badgeAwardService: badgeAwardService, + } +} + +// GetBadgeList list all badges +// @Summary list all badges group by group +// @Description list all badges group by group +// @Tags api-badge +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListResp} +// @Router /answer/api/v1/badges [get] +func (b *BadgeController) GetBadgeList(ctx *gin.Context) { + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := b.badgeService.ListByGroup(ctx, userID) + handler.HandleResponse(ctx, err, resp) +} + +// GetBadgeInfo get badge info +// @Summary get badge info +// @Description get badge info +// @Tags api-badge +// @Accept json +// @Produce json +// @Param id query string true "id" default(string) +// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} +// @Router /answer/api/v1/badge [get] +func (b *BadgeController) GetBadgeInfo(ctx *gin.Context) { + id := ctx.Query("id") + id = uid.DeShortID(id) + + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := b.badgeService.GetBadgeInfo(ctx, id, userID) + handler.HandleResponse(ctx, err, resp) +} + +// GetBadgeAwardList get badge award list +// @Summary get badge award list +// @Description get badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Param page query int false "page" +// @Param page_size query int false "page size" +// @Param badge_id query string true "badge id" +// @Param username query string false "only list the award by username" +// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} +// @Router /answer/api/v1/badge/awards/page [get] +func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { + req := &schema.GetBadgeAwardWithPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.BadgeID = uid.DeShortID(req.BadgeID) + + resp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} + +// GetAllBadgeAwardListByUsername get user badge award list +// @Summary get user badge award list +// @Description get user badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Param username query string true "user name" +// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} +// @Router /answer/api/v1/badge/user/awards [get] +func (b *BadgeController) GetAllBadgeAwardListByUsername(ctx *gin.Context) { + req := &schema.GetUserBadgeAwardListReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, total, err := b.badgeAwardService.GetUserBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} + +// GetRecentBadgeAwardListByUsername get user badge award list +// @Summary get user badge award list +// @Description get user badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Param username query string true "user name" +// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} +// @Router /answer/api/v1/badge/user/awards/recent [get] +func (b *BadgeController) GetRecentBadgeAwardListByUsername(ctx *gin.Context) { + req := &schema.GetUserBadgeAwardListReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.Limit = 10 + + resp, total, err := b.badgeAwardService.GetUserRecentBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} diff --git a/internal/controller/collection_controller.go b/internal/controller/collection_controller.go index c405acaad..78560be05 100644 --- a/internal/controller/collection_controller.go +++ b/internal/controller/collection_controller.go @@ -1,24 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/collection" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" ) // CollectionController collection controller type CollectionController struct { - collectionService *service.CollectionService + collectionService *collection.CollectionService } // NewCollectionController new controller -func NewCollectionController(collectionService *service.CollectionService) *CollectionController { +func NewCollectionController(collectionService *collection.CollectionService) *CollectionController { return &CollectionController{collectionService: collectionService} } @@ -38,19 +54,9 @@ func (cc *CollectionController) CollectionSwitch(ctx *gin.Context) { return } - dto := &schema.CollectionSwitchDTO{} - _ = copier.Copy(dto, req) - - dto.UserID = middleware.GetLoginUserIDFromContext(ctx) - - if converter.StringToInt64(req.ObjectID) < 1 { - return - } - if converter.StringToInt64(dto.UserID) < 1 { - handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) - return - } + req.ObjectID = uid.DeShortID(req.ObjectID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := cc.collectionService.CollectionSwitch(ctx, dto) + resp, err := cc.collectionService.CollectionSwitch(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/comment_controller.go b/internal/controller/comment_controller.go index aad50ed21..288e8aee4 100644 --- a/internal/controller/comment_controller.go +++ b/internal/controller/comment_controller.go @@ -1,27 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/comment" - "github.com/answerdev/answer/internal/service/rank" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" + "net/http" ) // CommentController comment controller type CommentController struct { - commentService *comment.CommentService - rankService *rank.RankService + commentService *comment.CommentService + rankService *rank.RankService + actionService *action.CaptchaService + rateLimitMiddleware *middleware.RateLimitMiddleware } // NewCommentController new controller func NewCommentController( commentService *comment.CommentService, - rankService *rank.RankService) *CommentController { - return &CommentController{commentService: commentService, rankService: rankService} + rankService *rank.RankService, + actionService *action.CaptchaService, + rateLimitMiddleware *middleware.RateLimitMiddleware, +) *CommentController { + return &CommentController{ + commentService: commentService, + rankService: rankService, + actionService: actionService, + rateLimitMiddleware: rateLimitMiddleware, + } } // AddComment add comment @@ -39,14 +75,55 @@ func (cc *CommentController) AddComment(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - + reject, rejectKey := cc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) + if reject { + return + } + defer func() { + // If status is not 200 means that the bad request has been returned, so the record should be cleared + if ctx.Writer.Status() != http.StatusOK { + cc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) + } + }() + req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.CommentAdd, + permission.CommentEdit, + permission.CommentDelete, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + linkUrlLimitUser := canList[3] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionComment, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + if !req.CanAdd { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } resp, err := cc.commentService.AddComment(ctx, req) + if !isAdmin || !linkUrlLimitUser { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID) + } handler.HandleResponse(ctx, err, resp) } @@ -67,12 +144,32 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := cc.commentService.RemoveComment(ctx, req) + err = cc.commentService.RemoveComment(ctx, req) + if !isAdmin { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -93,13 +190,39 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.CommentEdit, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] || cc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.CommentID) + linkUrlLimitUser := canList[1] + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := cc.commentService.UpdateComment(ctx, req) - handler.HandleResponse(ctx, err, nil) + if !req.IsAdmin || !linkUrlLimitUser { + captchaPass := cc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + resp, err := cc.commentService.UpdateComment(ctx, req) + if !req.IsAdmin || !linkUrlLimitUser { + cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } + handler.HandleResponse(ctx, err, resp) } // GetCommentWithPage get comment page @@ -118,8 +241,18 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - + req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.CommentEdit, + permission.CommentDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] resp, err := cc.commentService.GetCommentWithPage(ctx, req) handler.HandleResponse(ctx, err, resp) @@ -162,6 +295,16 @@ func (cc *CommentController) GetComment(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.CommentEdit, + permission.CommentDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] resp, err := cc.commentService.GetComment(ctx, req) handler.HandleResponse(ctx, err, resp) diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go new file mode 100644 index 000000000..939a2a092 --- /dev/null +++ b/internal/controller/connector_controller.go @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "fmt" + "net/http" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +const ( + commonRouterPrefix = "/answer/api/v1" + ConnectorLoginRouterPrefix = "/connector/login/" + ConnectorRedirectRouterPrefix = "/connector/redirect/" +) + +// ConnectorController comment controller +type ConnectorController struct { + siteInfoService siteinfo_common.SiteInfoCommonService + userExternalService *user_external_login.UserExternalLoginService + emailService *export.EmailService +} + +// NewConnectorController new controller +func NewConnectorController( + siteInfoService siteinfo_common.SiteInfoCommonService, + emailService *export.EmailService, + userExternalService *user_external_login.UserExternalLoginService, +) *ConnectorController { + return &ConnectorController{ + siteInfoService: siteInfoService, + userExternalService: userExternalService, + emailService: emailService, + } +} + +// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name +// We can't register specific router for each connector when application start, because the plugin status will be changed by admin. +// If the plugin is disabled, the router should be unavailable. +func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) { + slugName := ctx.Param("name") + var c plugin.Connector + _ = plugin.CallConnector(func(connector plugin.Connector) error { + if connector.ConnectorSlugName() == slugName { + c = connector + } + return nil + }) + if c == nil { + log.Errorf("connector %s not found", slugName) + ctx.Redirect(http.StatusFound, "/50x") + return + } + cc.ConnectorLogin(c)(ctx) +} + +func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) { + slugName := ctx.Param("name") + var c plugin.Connector + _ = plugin.CallConnector(func(connector plugin.Connector) error { + if connector.ConnectorSlugName() == slugName { + c = connector + } + return nil + }) + if c == nil { + log.Errorf("connector %s not found", slugName) + ctx.Redirect(http.StatusFound, "/50x") + return + } + cc.ConnectorRedirect(c)(ctx) +} + +func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) { + return func(ctx *gin.Context) { + general, err := cc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error(err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl, + commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) + redirectURL := connector.ConnectorSender(ctx, receiverURL) + if len(redirectURL) > 0 { + ctx.Redirect(http.StatusFound, redirectURL) + } + } +} + +func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) { + return func(ctx *gin.Context) { + siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site info failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl, + commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) + userInfo, err := connector.ConnectorReceiver(ctx, receiverURL) + if err != nil { + log.Errorf("connector received failed, error info: %v, response data is: %s", err, userInfo.MetaInfo) + ctx.Redirect(http.StatusFound, "/50x") + return + } + log.Debugf("connector received: %+v", userInfo) + u := &schema.ExternalLoginUserInfoCache{ + Provider: connector.ConnectorSlugName(), + ExternalID: userInfo.ExternalID, + DisplayName: userInfo.DisplayName, + Username: userInfo.Username, + Email: userInfo.Email, + Avatar: userInfo.Avatar, + MetaInfo: userInfo.MetaInfo, + } + resp, err := cc.userExternalService.ExternalLogin(ctx, u) + if err != nil { + log.Errorf("external login failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + if len(resp.ErrMsg) > 0 { + ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) + return + } + if len(resp.AccessToken) > 0 { + ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", + siteGeneral.SiteUrl, resp.AccessToken)) + } else { + ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s", + siteGeneral.SiteUrl, resp.BindingKey)) + } + } +} + +// ConnectorsInfo get all enabled connectors +// @Summary get all enabled connectors +// @Description get all enabled connectors +// @Tags PluginConnector +// @Security ApiKeyAuth +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp} +// @Router /answer/api/v1/connector/info [get] +func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) { + general, err := cc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + resp := make([]*schema.ConnectorInfoResp, 0) + _ = plugin.CallConnector(func(fn plugin.Connector) error { + connectorName := fn.ConnectorName() + resp = append(resp, &schema.ConnectorInfoResp{ + Name: connectorName.Translate(ctx), + Icon: fn.ConnectorLogoSVG(), + Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl, + commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()), + }) + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} + +// ExternalLoginBindingUserSendEmail external login binding user send email +// @Summary external login binding user send email +// @Description external login binding user send email +// @Tags PluginConnector +// @Accept json +// @Produce json +// @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email" +// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp} +// @Router /answer/api/v1/connector/binding/email [post] +func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) { + req := &schema.ExternalLoginBindingUserSendEmailReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// ConnectorsUserInfo get all connectors info about user +// @Summary get all connectors info about user +// @Description get all connectors info about user +// @Tags PluginConnector +// @Security ApiKeyAuth +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp} +// @Router /answer/api/v1/connector/user/info [get] +func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) { + general, err := cc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + userID := middleware.GetLoginUserIDFromContext(ctx) + + userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + userExternalLoginMapping := make(map[string]string) + for _, userInfo := range userInfoList { + userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID + } + + resp := make([]*schema.ConnectorUserInfoResp, 0) + _ = plugin.CallConnector(func(fn plugin.Connector) error { + externalID := userExternalLoginMapping[fn.ConnectorSlugName()] + connectorName := fn.ConnectorName() + resp = append(resp, &schema.ConnectorUserInfoResp{ + Name: connectorName.Translate(ctx), + Icon: fn.ConnectorLogoSVG(), + Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl, + commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()), + Binding: len(externalID) > 0, + ExternalID: externalID, + }) + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} + +// ExternalLoginUnbinding unbind external user login +// @Summary unbind external user login +// @Description unbind external user login +// @Tags PluginConnector +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/connector/user/unbinding [delete] +func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) { + req := &schema.ExternalLoginUnbindingReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 657760437..cbf80f7fa 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import "github.com/google/wire" @@ -19,5 +38,19 @@ var ProviderSetController = wire.NewSet( NewRankController, NewReasonController, NewNotificationController, - NewSiteinfoController, + NewSiteInfoController, + NewDashboardController, + NewUploadController, + NewActivityController, + NewTemplateController, + NewConnectorController, + NewUserCenterController, + NewPermissionController, + NewUserPluginController, + NewReviewController, + NewCaptchaController, + NewMetaController, + NewEmbedController, + NewBadgeController, + NewRenderController, ) diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go new file mode 100644 index 000000000..ea5f2aeae --- /dev/null +++ b/internal/controller/dashboard_controller.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/service/dashboard" + "github.com/gin-gonic/gin" +) + +type DashboardController struct { + dashboardService dashboard.DashboardService +} + +// NewDashboardController new controller +func NewDashboardController( + dashboardService dashboard.DashboardService, +) *DashboardController { + return &DashboardController{ + dashboardService: dashboardService, + } +} + +// DashboardInfo godoc +// @Summary DashboardInfo +// @Description DashboardInfo +// @Tags admin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Router /answer/admin/api/dashboard [get] +// @Success 200 {object} handler.RespBody +func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { + info, err := ac.dashboardService.Statistical(ctx) + handler.HandleResponse(ctx, err, gin.H{ + "info": info, + }) +} diff --git a/internal/controller/embed_controller.go b/internal/controller/embed_controller.go new file mode 100644 index 000000000..eb4803f99 --- /dev/null +++ b/internal/controller/embed_controller.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +type EmbedController struct { +} + +func NewEmbedController() *EmbedController { + return &EmbedController{} +} + +// GetEmbedConfig get embed plugin config +// @Summary get embed plugin config +// @Description get embed plugin config +// @Tags Plugin +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]plugin.EmbedConfig} +// @Router /answer/api/v1/embed/config [get] +func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) { + resp := make([]*plugin.EmbedConfig, 0) + + err := plugin.CallEmbed(func(embed plugin.Embed) (err error) { + resp, err = embed.GetEmbedConfigs(ctx) + return err + }) + + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/follow_controller.go b/internal/controller/follow_controller.go index 1ccc12201..aabf2d263 100644 --- a/internal/controller/follow_controller.go +++ b/internal/controller/follow_controller.go @@ -1,10 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/follow" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/follow" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" ) @@ -34,7 +54,7 @@ func (fc *FollowController) Follow(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - + req.ObjectID = uid.DeShortID(req.ObjectID) dto := &schema.FollowDTO{} _ = copier.Copy(dto, req) dto.UserID = middleware.GetLoginUserIDFromContext(ctx) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index a689fffc8..c7c607bdc 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -1,21 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( "encoding/json" - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { - translator i18n.Translator + translator i18n.Translator + siteInfoService siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator) *LangController { - return &LangController{translator: tr} +func NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController { + return &LangController{translator: tr, siteInfoService: siteInfoService} } // GetLangMapping get language config mapping @@ -33,15 +54,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) { handler.HandleResponse(ctx, nil, resp) } -// GetLangOptions Get language options +// GetAdminLangOptions Get language options // @Summary Get language options // @Description Get language options // @Security ApiKeyAuth // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} -// @Router /answer/api/v1/language/options [get] // @Router /answer/admin/api/language/options [get] -func (u *LangController) GetLangOptions(ctx *gin.Context) { - handler.HandleResponse(ctx, nil, schema.GetLangOptions) +func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// GetUserLangOptions Get language options +// @Summary Get language options +// @Description Get language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/language/options [get] +func (u *LangController) GetUserLangOptions(ctx *gin.Context) { + siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + options := translator.LanguageOptions + if len(siteInterfaceResp.Language) > 0 { + defaultOption := []*translator.LangOption{ + {Label: translator.DefaultLangOption, Value: translator.DefaultLangOption}, + } + options = append(defaultOption, options...) + } + handler.HandleResponse(ctx, nil, options) } diff --git a/internal/controller/meta_controller.go b/internal/controller/meta_controller.go new file mode 100644 index 000000000..624daf093 --- /dev/null +++ b/internal/controller/meta_controller.go @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/meta" + "github.com/apache/answer/pkg/uid" + "github.com/gin-gonic/gin" +) + +type MetaController struct { + metaService *meta.MetaService +} + +func NewMetaController( + metaService *meta.MetaService, +) *MetaController { + return &MetaController{ + metaService: metaService, + } +} + +// AddOrUpdateReaction add or update reaction +// @Summary add or update reaction +// @Description update reaction. if not exist, add one +// @Tags Meta +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateReactionReq true "reaction" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/meta/reaction [put] +func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) { + req := &schema.UpdateReactionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.ObjectID = uid.DeShortID(req.ObjectID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := mc.metaService.AddOrUpdateReaction(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// GetReaction get reaction +// @Summary get reaction +// @Description get reaction for an object +// @Tags Meta +// @Accept json +// @Produce json +// @Param object_id query string true "object_id" +// @Success 200 {object} handler.RespBody{data=schema.ReactionRespItem} +// @Router /answer/api/v1/meta/reaction [get] +func (mc *MetaController) GetReaction(ctx *gin.Context) { + req := &schema.GetReactionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.ObjectID = uid.DeShortID(req.ObjectID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := mc.metaService.GetReactionByObjectId(ctx, req) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index aa173be3e..849c9effa 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -1,21 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/notification" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/notification" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" "github.com/gin-gonic/gin" ) // NotificationController notification controller type NotificationController struct { notificationService *notification.NotificationService + rankService *rank.RankService } // NewNotificationController new controller -func NewNotificationController(notificationService *notification.NotificationService) *NotificationController { - return &NotificationController{notificationService: notificationService} +func NewNotificationController( + notificationService *notification.NotificationService, + rankService *rank.RankService, +) *NotificationController { + return &NotificationController{ + notificationService: notificationService, + rankService: rankService, + } } // GetRedDot @@ -28,9 +56,24 @@ func NewNotificationController(notificationService *notification.NotificationSer // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/status [get] func (nc *NotificationController) GetRedDot(ctx *gin.Context) { - userID := middleware.GetLoginUserIDFromContext(ctx) - RedDot, err := nc.notificationService.GetRedDot(ctx, userID) - handler.HandleResponse(ctx, err, RedDot) + req := &schema.GetRedDot{} + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := nc.notificationService.GetRedDot(ctx, req) + handler.HandleResponse(ctx, err, resp) } // ClearRedDot @@ -48,9 +91,22 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - RedDot, err := nc.notificationService.ClearRedDot(ctx, userID, req.TypeStr) - handler.HandleResponse(ctx, err, RedDot) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + resp, err := nc.notificationService.ClearRedDot(ctx, req) + handler.HandleResponse(ctx, err, resp) } // ClearUnRead @@ -69,7 +125,7 @@ func (nc *NotificationController) ClearUnRead(ctx *gin.Context) { return } userID := middleware.GetLoginUserIDFromContext(ctx) - err := nc.notificationService.ClearUnRead(ctx, userID, req.TypeStr) + err := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType) handler.HandleResponse(ctx, err, gin.H{}) } @@ -103,6 +159,7 @@ func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) { // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param type query string true "type" Enums(inbox,achievement) +// @Param inbox_type query string true "inbox_type" Enums(all,posts,invites,votes) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/notification/page [get] func (nc *NotificationController) GetList(ctx *gin.Context) { @@ -111,6 +168,6 @@ func (nc *NotificationController) GetList(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := nc.notificationService.GetList(ctx, req) + resp, err := nc.notificationService.GetNotificationPage(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/permission_controller.go b/internal/controller/permission_controller.go new file mode 100644 index 000000000..73daf5597 --- /dev/null +++ b/internal/controller/permission_controller.go @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/rank" + "github.com/gin-gonic/gin" +) + +type PermissionController struct { + rankService *rank.RankService +} + +// NewPermissionController new language controller. +func NewPermissionController(rankService *rank.RankService) *PermissionController { + return &PermissionController{rankService: rankService} +} + +// GetPermission check user permission +// @Summary check user permission +// @Description check user permission +// @Tags Permission +// @Security ApiKeyAuth +// @Param Authorization header string true "access-token" +// @Produce json +// @Param action query string true "permission key" Enums(question.add, question.edit, question.edit_without_review, question.delete, question.close, question.reopen, question.vote_up, question.vote_down, question.pin, question.unpin, question.hide, question.show, answer.add, answer.edit, answer.edit_without_review, answer.delete, answer.accept, answer.vote_up, answer.vote_down, answer.invite_someone_to_answer, comment.add, comment.edit, comment.delete, comment.vote_up, comment.vote_down, report.add, tag.add, tag.edit, tag.edit_slug_name, tag.edit_without_review, tag.delete, tag.synonym, link.url_limit, vote.detail, answer.audit, question.audit, tag.audit, tag.use_reserved_tag) +// @Success 200 {object} handler.RespBody{data=map[string]bool} +// @Router /answer/api/v1/permission [get] +func (u *PermissionController) GetPermission(ctx *gin.Context) { + req := &schema.GetPermissionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + userID := middleware.GetLoginUserIDFromContext(ctx) + ops, requireRanks, err := u.rankService.CheckOperationPermissionsForRanks(ctx, userID, req.Actions) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + lang := handler.GetLangByCtx(ctx) + mapping := make(map[string]*schema.GetPermissionResp, len(ops)) + for i, action := range req.Actions { + t := &schema.GetPermissionResp{HasPermission: ops[i]} + t.TrTip(lang, requireRanks[i]) + mapping[action] = t + } + handler.HandleResponse(ctx, err, mapping) +} diff --git a/internal/controller/plugin_captcha_controller.go b/internal/controller/plugin_captcha_controller.go new file mode 100644 index 000000000..c7c98d845 --- /dev/null +++ b/internal/controller/plugin_captcha_controller.go @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "encoding/json" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +// CaptchaController comment controller +type CaptchaController struct { +} + +// NewCaptchaController new controller +func NewCaptchaController() *CaptchaController { + return &CaptchaController{} +} + +type GetCaptchaConfigResp struct { + SlugName string `json:"slug_name"` + Config map[string]any `json:"config"` +} + +// GetCaptchaConfig get captcha config +func (uc *CaptchaController) GetCaptchaConfig(ctx *gin.Context) { + resp := &GetCaptchaConfigResp{} + _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { + resp.SlugName = fn.Info().SlugName + _ = json.Unmarshal([]byte(fn.GetConfig()), &resp.Config) + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} diff --git a/internal/controller/plugin_user_center_controller.go b/internal/controller/plugin_user_center_controller.go new file mode 100644 index 000000000..d426f45d5 --- /dev/null +++ b/internal/controller/plugin_user_center_controller.go @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "fmt" + "net/http" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +const ( + UserCenterLoginRouter = "/user-center/login/redirect" + UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect" +) + +// UserCenterController comment controller +type UserCenterController struct { + userCenterLoginService *user_external_login.UserCenterLoginService + siteInfoService siteinfo_common.SiteInfoCommonService +} + +// NewUserCenterController new controller +func NewUserCenterController( + userCenterLoginService *user_external_login.UserCenterLoginService, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *UserCenterController { + return &UserCenterController{ + userCenterLoginService: userCenterLoginService, + siteInfoService: siteInfoService, + } +} + +// UserCenterAgent get user center agent info +func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) { + resp := &schema.UserCenterAgentResp{} + resp.Enabled = plugin.UserCenterEnabled() + if !resp.Enabled { + handler.HandleResponse(ctx, nil, resp) + return + } + siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site info failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + resp.AgentInfo = &schema.AgentInfo{} + resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl, + commonRouterPrefix, UserCenterLoginRouter) + resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl, + commonRouterPrefix, UserCenterSignUpRedirectRouter) + + _ = plugin.CallUserCenter(func(uc plugin.UserCenter) error { + info := uc.Description() + resp.AgentInfo.Name = info.Name + resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx) + resp.AgentInfo.Icon = info.Icon + resp.AgentInfo.Url = info.Url + resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0) + resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem + items := uc.ControlCenterItems() + for _, item := range items { + resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{ + Name: item.Name, + Label: item.Label, + Url: item.Url, + }) + } + return nil + }) + + handler.HandleResponse(ctx, nil, resp) +} + +// UserCenterPersonalBranding get user center personal user info +func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) { + req := &schema.GetOtherUserInfoByUsernameReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username) + handler.HandleResponse(ctx, err, resp) +} + +func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) { + var redirectURL string + _ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error { + info := userCenter.Description() + redirectURL = info.LoginRedirectURL + return nil + }) + ctx.Redirect(http.StatusFound, redirectURL) +} + +func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) { + var redirectURL string + _ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error { + info := userCenter.Description() + redirectURL = info.LoginRedirectURL + return nil + }) + ctx.Redirect(http.StatusFound, redirectURL) +} + +func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) { + siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site info failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + userCenter, ok := plugin.GetUserCenter() + if !ok { + ctx.Redirect(http.StatusFound, "/404") + return + } + userInfo, err := userCenter.LoginCallback(ctx) + if err != nil { + log.Error(err) + if !ctx.IsAborted() { + ctx.Redirect(http.StatusFound, "/50x") + } + return + } + + resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo) + if err != nil { + log.Errorf("external login failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + if len(resp.ErrMsg) > 0 { + ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) + return + } + userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken) + ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", + siteGeneral.SiteUrl, resp.AccessToken)) +} + +func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) { + siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site info failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + userCenter, ok := plugin.GetUserCenter() + if !ok { + ctx.Redirect(http.StatusFound, "/404") + return + } + userInfo, err := userCenter.SignUpCallback(ctx) + if err != nil { + log.Error(err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + + resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo) + if err != nil { + log.Errorf("external login failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + if len(resp.ErrMsg) > 0 { + ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg)) + return + } + userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken) + ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s", + siteGeneral.SiteUrl, resp.AccessToken)) +} + +// UserCenterUserSettings user center user settings +func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) { + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID) + handler.HandleResponse(ctx, err, resp) +} + +// UserCenterAdminFunctionAgent user center admin function agent +func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) { + resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 620f1e41f..4b1869fa7 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -1,35 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "context" - - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/pkg/converter" + "net/http" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) // QuestionController question controller type QuestionController struct { - questionService *service.QuestionService - rankService *rank.RankService + questionService *content.QuestionService + answerService *content.AnswerService + rankService *rank.RankService + siteInfoService siteinfo_common.SiteInfoCommonService + actionService *action.CaptchaService + rateLimitMiddleware *middleware.RateLimitMiddleware } // NewQuestionController new controller -func NewQuestionController(questionService *service.QuestionService, rankService *rank.RankService) *QuestionController { - return &QuestionController{questionService: questionService, rankService: rankService} +func NewQuestionController( + questionService *content.QuestionService, + answerService *content.AnswerService, + rankService *rank.RankService, + siteInfoService siteinfo_common.SiteInfoCommonService, + actionService *action.CaptchaService, + rateLimitMiddleware *middleware.RateLimitMiddleware, +) *QuestionController { + return &QuestionController{ + questionService: questionService, + answerService: answerService, + rankService: rankService, + siteInfoService: siteInfoService, + actionService: actionService, + rateLimitMiddleware: rateLimitMiddleware, + } } // RemoveQuestion delete question // @Summary delete question // @Description delete question -// @Tags api-question +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth @@ -41,20 +85,83 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } + req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + req.IsAdmin = middleware.GetIsAdminFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionDelete, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } + err = qc.questionService.RemoveQuestion(ctx, req) + if !isAdmin { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionDelete, req.UserID) + } + handler.HandleResponse(ctx, err, nil) +} - err := qc.questionService.RemoveQuestion(ctx, req) +// OperationQuestion Operation question +// @Summary Operation question +// @Description Operation question \n operation [pin unpin hide show] +// @Tags Question +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.OperationQuestionReq true "question" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/question/operation [put] +func (qc *QuestionController) OperationQuestion(ctx *gin.Context) { + req := &schema.OperationQuestionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.ID = uid.DeShortID(req.ID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionPin, + permission.QuestionUnPin, + permission.QuestionHide, + permission.QuestionShow, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanPin = canList[0] + req.CanList = canList[1] + if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + err = qc.questionService.OperationQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } // CloseQuestion Close question // @Summary Close question // @Description Close question -// @Tags api-question +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth @@ -66,37 +173,127 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) - err := qc.questionService.CloseQuestion(ctx, req) + req.ID = uid.DeShortID(req.ID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = qc.questionService.CloseQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } -// GetQuestion godoc -// @Summary GetQuestion Question -// @Description GetQuestion Question -// @Tags api-question +// ReopenQuestion reopen question +// @Summary reopen question +// @Description reopen question +// @Tags Question +// @Accept json +// @Produce json // @Security ApiKeyAuth +// @Param data body schema.ReopenQuestionReq true "question" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/question/reopen [put] +func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) { + req := &schema.ReopenQuestionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.QuestionID = uid.DeShortID(req.QuestionID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = qc.questionService.ReopenQuestion(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// GetQuestion get question details +// @Summary get question details +// @Description get question details +// @Tags Question // @Accept json // @Produce json // @Param id query string true "Question TagID" default(1) // @Success 200 {string} string "" // @Router /answer/api/v1/question/info [get] -func (qc *QuestionController) GetQuestion(c *gin.Context) { - id := c.Query("id") - ctx := context.Background() - userID := middleware.GetLoginUserIDFromContext(c) - info, err := qc.questionService.GetQuestion(ctx, id, userID, true) +func (qc *QuestionController) GetQuestion(ctx *gin.Context) { + id := ctx.Query("id") + id = uid.DeShortID(id) + userID := middleware.GetLoginUserIDFromContext(ctx) + req := schema.QuestionPermission{} + canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{ + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionClose, + permission.QuestionReopen, + permission.QuestionPin, + permission.QuestionUnPin, + permission.QuestionHide, + permission.QuestionShow, + permission.AnswerInviteSomeoneToAnswer, + permission.QuestionUnDelete, + }) if err != nil { - handler.HandleResponse(c, err, nil) + handler.HandleResponse(ctx, err, nil) + return + } + objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id) + + req.CanEdit = canList[0] || objectOwner + req.CanDelete = canList[1] + req.CanClose = canList[2] + req.CanReopen = canList[3] + req.CanPin = canList[4] + req.CanUnPin = canList[5] + req.CanHide = canList[6] + req.CanShow = canList[7] + req.CanInviteOtherToAnswer = canList[8] + req.CanRecover = canList[9] + + info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) return } - handler.HandleResponse(c, nil, info) + if handler.GetEnableShortID(ctx) { + info.ID = uid.EnShortID(info.ID) + } + handler.HandleResponse(ctx, nil, info) +} + +// GetQuestionInviteUserInfo get question invite user info +// @Summary get question invite user info +// @Description get question invite user info +// @Tags Question +// @Accept json +// @Produce json +// @Param id query string true "Question ID" default(1) +// @Success 200 {string} string "" +// @Router /answer/api/v1/question/invite [get] +func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) { + questionID := uid.DeShortID(ctx.Query("id")) + resp, err := qc.questionService.InviteUserInfo(ctx, questionID) + handler.HandleResponse(ctx, err, resp) + } // SimilarQuestion godoc // @Summary Search Similar Question // @Description Search Similar Question -// @Tags api-question +// @Tags Question // @Accept json // @Produce json // @Param question_id query string true "question_id" default() @@ -104,6 +301,7 @@ func (qc *QuestionController) GetQuestion(c *gin.Context) { // @Router /answer/api/v1/question/similar/tag [get] func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) { questionID := ctx.Query("question_id") + questionID = uid.DeShortID(questionID) userID := middleware.GetLoginUserIDFromContext(ctx) list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID) if err != nil { @@ -114,68 +312,69 @@ func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) { "list": list, "count": count, }) - } -// Index godoc -// @Summary SearchQuestionList -// @Description SearchQuestionList
"order" Enums(newest, active,frequent,score,unanswered) -// @Tags api-question +// QuestionPage get questions by page +// @Summary get questions by page +// @Description get questions by page +// @Tags Question // @Accept json // @Produce json -// @Param data body schema.QuestionSearch true "QuestionSearch" -// @Success 200 {string} string "" +// @Param data body schema.QuestionPageReq true "QuestionPageReq" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} // @Router /answer/api/v1/question/page [get] -func (qc *QuestionController) Index(ctx *gin.Context) { - req := &schema.QuestionSearch{} +func (qc *QuestionController) QuestionPage(ctx *gin.Context) { + req := &schema.QuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - list, count, err := qc.questionService.SearchList(ctx, req, userID) + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + questions, total, err := qc.questionService.GetQuestionPage(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } - handler.HandleResponse(ctx, nil, gin.H{ - "list": list, - "count": count, - }) + if pager.ValPageOutOfRange(total, req.Page, req.PageSize) { + handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil) + return + } + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } -// SearchList godoc -// @Summary SearchQuestionList -// @Description SearchQuestionList -// @Tags api-question +// QuestionRecommendPage get recommend questions by page +// @Summary get recommend questions by page +// @Description get recommend questions by page +// @Tags Question // @Accept json // @Produce json -// @Param data body schema.QuestionSearch true "QuestionSearch" -// @Router /answer/api/v1/question/search [post] -// @Success 200 {string} string "" -func (qc *QuestionController) SearchList(c *gin.Context) { - Request := new(schema.QuestionSearch) - err := c.BindJSON(Request) - if err != nil { - handler.HandleResponse(c, err, nil) +// @Param data body schema.QuestionPageReq true "QuestionPageReq" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} +// @Router /answer/api/v1/question/recommend/page [get] +func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { + req := &schema.QuestionPageReq{} + if handler.BindAndCheck(ctx, req) { return } - ctx := context.Background() - userID := middleware.GetLoginUserIDFromContext(c) - list, count, err := qc.questionService.SearchList(ctx, Request, userID) + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + if req.LoginUserID == "" { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } + + questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req) if err != nil { - handler.HandleResponse(c, err, nil) + handler.HandleResponse(ctx, err, nil) return } - handler.HandleResponse(c, nil, gin.H{ - "list": list, - "count": count, - }) + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } // AddQuestion add question // @Summary add question // @Description add question -// @Tags api-question +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth @@ -184,24 +383,237 @@ func (qc *QuestionController) SearchList(c *gin.Context) { // @Router /answer/api/v1/question [post] func (qc *QuestionController) AddQuestion(ctx *gin.Context) { req := &schema.QuestionAdd{} - if handler.BindAndCheck(ctx, req) { + errFields := handler.BindAndCheckReturnErr(ctx, req) + if ctx.IsAborted() { + return + } + reject, rejectKey := qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) + if reject { return } + defer func() { + // If status is not 200 means that the bad request has been returned, so the record should be cleared + if ctx.Writer.Status() != http.StatusOK { + qc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) + } + }() + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ + permission.QuestionAdd, + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionClose, + permission.QuestionReopen, + permission.TagUseReservedTag, + permission.TagAdd, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + linkUrlLimitUser := canList[7] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + req.CanClose = canList[3] + req.CanReopen = canList[4] + req.CanUseReservedTag = canList[5] + req.CanAddTag = canList[6] + if !req.CanAdd { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + // can add tag + hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !req.CanAddTag && hasNewTag { + lang := handler.GetLang(ctx) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) + handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) + return + } - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + errList, err := qc.questionService.CheckAddQuestion(ctx, req) + if err != nil { + errlist, ok := errList.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) return } + req.UserAgent = ctx.GetHeader("User-Agent") + req.IP = ctx.ClientIP() + resp, err := qc.questionService.AddQuestion(ctx, req) + if err != nil { + errlist, ok := resp.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) + } + handler.HandleResponse(ctx, err, resp) +} + +// AddQuestionByAnswer add question +// @Summary add question and answer +// @Description add question and answer +// @Tags Question +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.QuestionAddByAnswer true "question" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/question/answer [post] +func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) { + req := &schema.QuestionAddByAnswer{} + errFields := handler.BindAndCheckReturnErr(ctx, req) + if ctx.IsAborted() { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAdd, + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionClose, + permission.QuestionReopen, + permission.TagUseReservedTag, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + linkUrlLimitUser := canList[6] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionQuestion, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + req.CanClose = canList[3] + req.CanReopen = canList[4] + req.CanUseReservedTag = canList[5] + if !req.CanAdd { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + questionReq := new(schema.QuestionAdd) + err = copier.Copy(questionReq, req) + if err != nil { + handler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil) + return + } + errList, err := qc.questionService.CheckAddQuestion(ctx, questionReq) + if err != nil { + errlist, ok := errList.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + + req.UserAgent = ctx.GetHeader("User-Agent") + req.IP = ctx.ClientIP() + resp, err := qc.questionService.AddQuestion(ctx, questionReq) + if err != nil { + errlist, ok := resp.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionQuestion, req.UserID) + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + //add the question id to the answer + questionInfo, ok := resp.(*schema.QuestionInfoResp) + if ok { + answerReq := &schema.AnswerAddReq{} + answerReq.QuestionID = uid.DeShortID(questionInfo.ID) + answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx) + answerReq.Content = req.AnswerContent + answerReq.HTML = req.AnswerHTML + answerID, err := qc.answerService.Insert(ctx, answerReq) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !has { + handler.HandleResponse(ctx, nil, nil) + return + } + handler.HandleResponse(ctx, err, gin.H{ + "info": info, + "question": questionInfo, + }) + return + } + handler.HandleResponse(ctx, err, resp) } // UpdateQuestion update question // @Summary update question // @Description update question -// @Tags api-question +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth @@ -210,58 +622,203 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) { // @Router /answer/api/v1/question [put] func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) { req := &schema.QuestionUpdate{} - if handler.BindAndCheck(ctx, req) { + errFields := handler.BindAndCheckReturnErr(ctx, req) + if ctx.IsAborted() { return } + req.ID = uid.DeShortID(req.ID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionEditWithoutReview, + permission.TagUseReservedTag, + permission.TagAdd, + permission.LinkUrlLimit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + linkUrlLimitUser := canList[5] + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin || !linkUrlLimitUser { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEdit, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID) + req.CanEdit = canList[0] || objectOwner + req.CanDelete = canList[1] + req.NoNeedReview = canList[2] || objectOwner + req.CanUseReservedTag = canList[3] + req.CanAddTag = canList[4] + if !req.CanEdit { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } - if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req) + if err != nil { + errFields = append(errFields, errlist...) + } + + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + + // can add tag + hasNewTag, err := qc.questionService.HasNewTag(ctx, req.Tags) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !req.CanAddTag && hasNewTag { + lang := handler.GetLang(ctx) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[4]}) + handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) return } resp, err := qc.questionService.UpdateQuestion(ctx, req) - handler.HandleResponse(ctx, err, resp) + if err != nil { + handler.HandleResponse(ctx, err, resp) + return + } + respInfo, ok := resp.(*schema.QuestionInfoResp) + if !ok { + handler.HandleResponse(ctx, err, resp) + return + } + if !isAdmin || !linkUrlLimitUser { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID) + } + handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{UrlTitle: respInfo.UrlTitle, WaitForReview: !req.NoNeedReview}) } -// CloseMsgList close question msg list -// @Summary close question msg list -// @Description close question msg list -// @Tags api-question +// QuestionRecover recover deleted question +// @Summary recover deleted question +// @Description recover deleted question +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth +// @Param data body schema.QuestionRecoverReq true "question" // @Success 200 {object} handler.RespBody -// @Router /answer/api/v1/question/closemsglist [get] -func (qc *QuestionController) CloseMsgList(ctx *gin.Context) { - resp, err := qc.questionService.CloseMsgList(ctx, handler.GetLang(ctx)) - handler.HandleResponse(ctx, err, resp) +// @Router /answer/api/v1/question/recover [post] +func (qc *QuestionController) QuestionRecover(ctx *gin.Context) { + req := &schema.QuestionRecoverReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.QuestionID = uid.DeShortID(req.QuestionID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionUnDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = qc.questionService.RecoverQuestion(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateQuestionInviteUser update question invite user +// @Summary update question invite user +// @Description update question invite user +// @Tags Question +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.QuestionUpdateInviteUser true "question" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/question/invite [put] +func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) { + req := &schema.QuestionUpdateInviteUser{} + errFields := handler.BindAndCheckReturnErr(ctx, req) + if ctx.IsAborted() { + return + } + if len(errFields) > 0 { + handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields) + return + } + req.ID = uid.DeShortID(req.ID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := qc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionInvitationAnswer, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.AnswerInviteSomeoneToAnswer, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + req.CanInviteOtherToAnswer = canList[0] + if !req.CanInviteOtherToAnswer { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + err = qc.questionService.UpdateQuestionInviteUser(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !isAdmin { + qc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionInvitationAnswer, req.UserID) + } + handler.HandleResponse(ctx, nil, nil) } -// SearchByTitleLike add question title like -// @Summary add question title like -// @Description add question title like -// @Tags api-question +// GetSimilarQuestions fuzzy query similar questions based on title +// @Summary fuzzy query similar questions based on title +// @Description fuzzy query similar questions based on title +// @Tags Question // @Accept json // @Produce json // @Security ApiKeyAuth // @Param title query string true "title" default(string) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/question/similar [get] -func (qc *QuestionController) SearchByTitleLike(ctx *gin.Context) { +func (qc *QuestionController) GetSimilarQuestions(ctx *gin.Context) { title := ctx.Query("title") - userID := middleware.GetLoginUserIDFromContext(ctx) - resp, err := qc.questionService.SearchByTitleLike(ctx, title, userID) + resp, err := qc.questionService.GetQuestionsByTitle(ctx, title) handler.HandleResponse(ctx, err, resp) } // UserTop godoc // @Summary UserTop // @Description UserTop -// @Tags api-question +// @Tags Question // @Accept json // @Produce json -// @Security ApiKeyAuth // @Param username query string true "username" default(string) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/qa/top [get] @@ -275,151 +832,168 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) { }) } -// UserList godoc -// @Summary UserList -// @Description UserList -// @Tags api-question +// PersonalQuestionPage list personal questions +// @Summary list personal questions +// @Description list personal questions +// @Tags Personal // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" default(string) // @Param order query string true "order" Enums(newest,score) // @Param page query string true "page" default(0) -// @Param pagesize query string true "pagesize" default(20) +// @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /personal/question/page [get] -func (qc *QuestionController) UserList(ctx *gin.Context) { - userName := ctx.Query("username") - order := ctx.Query("order") - pageStr := ctx.Query("page") - pageSizeStr := ctx.Query("pagesize") - page := converter.StringToInt(pageStr) - pageSize := converter.StringToInt(pageSizeStr) - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) +func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) { + req := &schema.PersonalQuestionPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + resp, err := qc.questionService.PersonalQuestionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// UserAnswerList godoc -// @Summary UserAnswerList -// @Description UserAnswerList -// @Tags api-answer +// PersonalAnswerPage list personal answers +// @Summary list personal answers +// @Description list personal answers +// @Tags Personal // @Accept json // @Produce json // @Security ApiKeyAuth // @Param username query string true "username" default(string) // @Param order query string true "order" Enums(newest,score) // @Param page query string true "page" default(0) -// @Param pagesize query string true "pagesize" default(20) +// @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/answer/page [get] -func (qc *QuestionController) UserAnswerList(ctx *gin.Context) { - userName := ctx.Query("username") - order := ctx.Query("order") - pageStr := ctx.Query("page") - pageSizeStr := ctx.Query("pagesize") - page := converter.StringToInt(pageStr) - pageSize := converter.StringToInt(pageSizeStr) - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) +func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) { + req := &schema.PersonalAnswerPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + resp, err := qc.questionService.PersonalAnswerPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// UserCollectionList godoc -// @Summary UserCollectionList -// @Description UserCollectionList +// PersonalCollectionPage list personal collections +// @Summary list personal collections +// @Description list personal collections // @Tags Collection // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query string true "page" default(0) -// @Param pagesize query string true "pagesize" default(20) +// @Param page_size query string true "page_size" default(20) // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/personal/collection/page [get] -func (qc *QuestionController) UserCollectionList(ctx *gin.Context) { - pageStr := ctx.Query("page") - pageSizeStr := ctx.Query("pagesize") - page := converter.StringToInt(pageStr) - pageSize := converter.StringToInt(pageSizeStr) - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) +func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) { + req := &schema.PersonalCollectionPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := qc.questionService.PersonalCollectionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// CmsSearchList godoc -// @Summary CmsSearchList -// @Description Status:[available,closed,deleted] +// AdminQuestionPage admin question page +// @Summary AdminQuestionPage admin question page +// @Description Status:[available,closed,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" -// @Param status query string false "user status" Enums(available, closed, deleted) +// @Param status query string false "user status" Enums(available, closed, deleted, pending) +// @Param query query string false "question id or title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/page [get] -func (qc *QuestionController) CmsSearchList(ctx *gin.Context) { - req := &schema.CmsQuestionSearch{} +func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) { + req := &schema.AdminQuestionPageReq{} if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.CmsSearchList(ctx, req, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := qc.questionService.AdminQuestionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// CmsSearchAnswerList godoc -// @Summary CmsSearchList -// @Description Status:[available,deleted] +// AdminAnswerPage admin answer page +// @Summary AdminAnswerPage admin answer page +// @Description Status:[available,deleted,pending] // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth // @Param page query int false "page size" // @Param page_size query int false "page size" -// @Param status query string false "user status" Enums(available,deleted) +// @Param status query string false "user status" Enums(available,deleted,pending) +// @Param query query string false "answer id or question title" +// @Param question_id query string false "question id" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/page [get] -func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) { - req := &entity.CmsAnswerSearch{} +func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) { + req := &schema.AdminAnswerPageReq{} if handler.BindAndCheck(ctx, req) { return } - userID := middleware.GetLoginUserIDFromContext(ctx) - questionList, count, err := qc.questionService.CmsSearchAnswerList(ctx, req, userID) - handler.HandleResponse(ctx, err, gin.H{ - "list": questionList, - "count": count, - }) + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := qc.questionService.AdminAnswerPage(ctx, req) + handler.HandleResponse(ctx, err, resp) } -// AdminSetQuestionStatus godoc -// @Summary AdminSetQuestionStatus -// @Description Status:[available,closed,deleted] +// AdminUpdateQuestionStatus update question status +// @Summary update question status +// @Description update question status // @Tags admin // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.AdminSetQuestionStatusRequest true "AdminSetQuestionStatusRequest" -// @Router /answer/admin/api/question/status [put] +// @Param data body schema.AdminUpdateQuestionStatusReq true "AdminUpdateQuestionStatusReq" // @Success 200 {object} handler.RespBody -func (qc *QuestionController) AdminSetQuestionStatus(ctx *gin.Context) { - req := &schema.AdminSetQuestionStatusRequest{} +// @Router /answer/admin/api/question/status [put] +func (qc *QuestionController) AdminUpdateQuestionStatus(ctx *gin.Context) { + req := &schema.AdminUpdateQuestionStatusReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.QuestionID = uid.DeShortID(req.QuestionID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + err := qc.questionService.AdminSetQuestionStatus(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// GetQuestionLink get question link +// @Summary get question link +// @Description get question link +// @Tags Question +// @Param data query schema.GetQuestionLinkReq true "GetQuestionLinkReq" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} +// @Router /answer/api/v1/question/link [get] +func (qc *QuestionController) GetQuestionLink(ctx *gin.Context) { + req := &schema.GetQuestionLinkReq{} if handler.BindAndCheck(ctx, req) { return } - err := qc.questionService.AdminSetQuestionStatus(ctx, req.QuestionID, req.StatusStr) - handler.HandleResponse(ctx, err, gin.H{}) + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + req.QuestionID = uid.DeShortID(req.QuestionID) + questions, total, err := qc.questionService.GetQuestionLink(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } diff --git a/internal/controller/rank_controller.go b/internal/controller/rank_controller.go index b3340f61d..b330ca3b8 100644 --- a/internal/controller/rank_controller.go +++ b/internal/controller/rank_controller.go @@ -1,10 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/rank" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/rank" "github.com/gin-gonic/gin" ) @@ -27,7 +46,7 @@ func NewRankController( // @Param page query int false "page" // @Param page_size query int false "page size" // @Param username query string false "username" -// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalWithPageResp}} +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalPageResp}} // @Router /answer/api/v1/personal/rank/page [get] func (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) { req := &schema.GetRankPersonalWithPageReq{} @@ -37,6 +56,6 @@ func (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := cc.rankService.GetRankPersonalWithPage(ctx, req) + resp, err := cc.rankService.GetRankPersonalPage(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/reason_controller.go b/internal/controller/reason_controller.go index 7dd806322..7ec07cb58 100644 --- a/internal/controller/reason_controller.go +++ b/internal/controller/reason_controller.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/reason" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/reason" "github.com/gin-gonic/gin" ) diff --git a/internal/controller/render_controller.go b/internal/controller/render_controller.go new file mode 100644 index 000000000..63725ae0a --- /dev/null +++ b/internal/controller/render_controller.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +type RenderController struct { +} + +func NewRenderController() *RenderController { + return &RenderController{} +} + +// GetRenderConfig godoc +// @Summary GetRenderConfig +// @Description GetRenderConfig +// @Tags PluginRender +// @Accept json +// @Produce json +// @Router /answer/api/v1/render/config [get] +// @Success 200 {object} handler.RespBody{data=plugin.RenderConfig} +func (c *RenderController) GetRenderConfig(ctx *gin.Context) { + var resp *plugin.RenderConfig + + _ = plugin.CallRender(func(render plugin.Render) (err error) { + resp = render.GetRenderConfig(ctx) + return nil + }) + + handler.HandleResponse(ctx, nil, resp) +} diff --git a/internal/controller/report_controller.go b/internal/controller/report_controller.go index 1067ee524..28048dd3d 100644 --- a/internal/controller/report_controller.go +++ b/internal/controller/report_controller.go @@ -1,12 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/internal/service/report" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/report" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) @@ -15,17 +40,25 @@ import ( type ReportController struct { reportService *report.ReportService rankService *rank.RankService + actionService *action.CaptchaService } // NewReportController new controller -func NewReportController(reportService *report.ReportService, rankService *rank.RankService) *ReportController { - return &ReportController{reportService: reportService, rankService: rankService} +func NewReportController( + reportService *report.ReportService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *ReportController { + return &ReportController{ + reportService: reportService, + rankService: rankService, + actionService: actionService, + } } // AddReport add report // @Summary add report // @Description add report
source (question, answer, comment, user) -// @Security ApiKeyAuth // @Tags Report // @Accept json // @Produce json @@ -38,31 +71,84 @@ func (rc *ReportController) AddReport(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - + req.ObjectID = uid.DeShortID(req.ObjectID) req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := rc.rankService.CheckRankPermission(ctx, req.UserID, rank.ReportAddRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := rc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionReport, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := rc.reportService.AddReport(ctx, req) + err = rc.reportService.AddReport(ctx, req) + if !isAdmin { + rc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionReport, req.UserID) + } handler.HandleResponse(ctx, err, nil) } -// GetReportTypeList get report type list -// @Summary get report type list -// @Description get report type list +// GetUnreviewedReportPostPage get unreviewed report post page +// @Summary get unreviewed report post page +// @Description get unreviewed report post page // @Tags Report +// @Accept json // @Produce json -// @Param source query string true "report source" Enums(question, answer, comment, user) -// @Success 200 {object} handler.RespBody{data=[]schema.GetReportTypeResp} -// @Router /answer/api/v1/report/type/list [get] -func (rc *ReportController) GetReportTypeList(ctx *gin.Context) { - req := &schema.GetReportListReq{} +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetReportListPageResp}} +// @Router /answer/api/v1/report/unreviewed/post [get] +func (rc *ReportController) GetUnreviewedReportPostPage(ctx *gin.Context) { + req := &schema.GetUnreviewedReportPostPageReq{} if handler.BindAndCheck(ctx, req) { return } - resp, err := rc.reportService.GetReportTypeList(ctx, handler.GetLang(ctx), req) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := rc.reportService.GetUnreviewedReportPostPage(ctx, req) handler.HandleResponse(ctx, err, resp) } + +// ReviewReport review report +// @Summary review report +// @Description review report +// @Tags Report +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.ReviewReportReq true "flag" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/report/review [put] +func (rc *ReportController) ReviewReport(ctx *gin.Context) { + req := &schema.ReviewReportReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + if !req.IsAdmin { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + + err := rc.reportService.ReviewReport(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/review_controller.go b/internal/controller/review_controller.go new file mode 100644 index 000000000..0e897d9ba --- /dev/null +++ b/internal/controller/review_controller.go @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +// ReviewController review controller +type ReviewController struct { + reviewService *review.ReviewService + rankService *rank.RankService + actionService *action.CaptchaService +} + +// NewReviewController new controller +func NewReviewController( + reviewService *review.ReviewService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *ReviewController { + return &ReviewController{ + reviewService: reviewService, + rankService: rankService, + actionService: actionService, + } +} + +// GetUnreviewedPostPage get unreviewed post page +// @Summary get unreviewed post page +// @Description get unreviewed post page +// @Tags Review +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Param object_id query string false "object_id" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedPostPageResp}} +// @Router /answer/api/v1/review/pending/post/page [get] +func (rc *ReviewController) GetUnreviewedPostPage(ctx *gin.Context) { + req := &schema.GetUnreviewedPostPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + req.ReviewerMapping = make(map[string]string) + _ = plugin.CallReviewer(func(base plugin.Reviewer) error { + info := base.Info() + req.ReviewerMapping[info.SlugName] = info.Name.Translate(ctx) + return nil + }) + + resp, err := rc.reviewService.GetUnreviewedPostPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateReview update review +// @Summary update review +// @Description update review +// @Tags Review +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateReviewReq true "review" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/review/pending/post [put] +func (rc *ReviewController) UpdateReview(ctx *gin.Context) { + req := &schema.UpdateReviewReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + if !req.IsAdmin { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + + err := rc.reviewService.UpdateReview(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/revision_controller.go b/internal/controller/revision_controller.go index 5f30b70eb..13bee19c4 100644 --- a/internal/controller/revision_controller.go +++ b/internal/controller/revision_controller.go @@ -1,22 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // RevisionController revision controller type RevisionController struct { - revisionListService *service.RevisionService + revisionListService *content.RevisionService + rankService *rank.RankService } // NewRevisionController new controller -func NewRevisionController(revisionListService *service.RevisionService) *RevisionController { - return &RevisionController{revisionListService: revisionListService} +func NewRevisionController( + revisionListService *content.RevisionService, + rankService *rank.RankService, +) *RevisionController { + return &RevisionController{ + revisionListService: revisionListService, + rankService: rankService, + } } // GetRevisionList godoc @@ -33,11 +66,157 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) return } - + objectID = uid.DeShortID(objectID) req := &schema.GetRevisionListReq{ ObjectID: objectID, } resp, err := rc.revisionListService.GetRevisionList(ctx, req) + list := make([]schema.GetRevisionResp, 0) + for _, item := range resp { + if item.Status == entity.RevisionNormalStatus || item.Status == entity.RevisionReviewPassStatus { + list = append(list, item) + } + } + handler.HandleResponse(ctx, err, list) +} + +// GetUnreviewedRevisionList godoc +// @Summary get unreviewed revision list +// @Description get unreviewed revision list +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Param page query string true "page id" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUnreviewedRevisionResp}} +// @Router /answer/api/v1/revisions/unreviewed [get] +func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) { + req := &schema.RevisionSearch{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + resp, err := rc.revisionListService.GetUnreviewedRevisionPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// RevisionAudit godoc +// @Summary revision audit +// @Description revision audit operation:approve or reject +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.RevisionAuditReq true "audit" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/revisions/audit [put] +func (rc *RevisionController) RevisionAudit(ctx *gin.Context) { + req := &schema.RevisionAuditReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + + err = rc.revisionListService.RevisionAudit(ctx, req) + handler.HandleResponse(ctx, err, gin.H{}) +} + +// CheckCanUpdateRevision check can update revision +// @Summary check can update revision +// @Description check can update revision +// @Tags Revision +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id query string true "id" default(string) +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/revisions/edit/check [get] +func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) { + req := &schema.CheckCanQuestionUpdate{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + action := "" + req.ID = uid.DeShortID(req.ID) + objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID) + switch objectTypeStr { + case constant.QuestionObjectType: + action = permission.QuestionEdit + case constant.AnswerObjectType: + action = permission.AnswerEdit + case constant.TagObjectType: + action = permission.TagEdit + default: + handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil) + return + } + + can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, action, req.ID) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + resp, err := rc.revisionListService.CheckCanUpdateRevision(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// GetReviewingType get reviewing type +// @Summary get reviewing type +// @Description get reviewing type +// @Tags Revision +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} handler.RespBody{data=[]schema.GetReviewingTypeResp} +// @Router /answer/api/v1/reviewing/type [get] +func (rc *RevisionController) GetReviewingType(ctx *gin.Context) { + req := &schema.GetReviewingTypeReq{} + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.QuestionAudit, + permission.AnswerAudit, + permission.TagAudit, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanReviewQuestion = canList[0] + req.CanReviewAnswer = canList[1] + req.CanReviewTag = canList[2] + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + + resp, err := rc.revisionListService.GetReviewingType(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller/search_controller.go b/internal/controller/search_controller.go index 5b6583516..64acbe252 100644 --- a/internal/controller/search_controller.go +++ b/internal/controller/search_controller.go @@ -1,21 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/plugin" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" ) // SearchController tag controller type SearchController struct { - searchService *service.SearchService + searchService *content.SearchService + actionService *action.CaptchaService } // NewSearchController new controller -func NewSearchController(searchService *service.SearchService) *SearchController { - return &SearchController{searchService: searchService} +func NewSearchController( + searchService *content.SearchService, + actionService *action.CaptchaService, +) *SearchController { + return &SearchController{ + searchService: searchService, + actionService: actionService, + } } // Search godoc @@ -26,7 +59,7 @@ func NewSearchController(searchService *service.SearchService) *SearchController // @Security ApiKeyAuth // @Param q query string true "query string" // @Param order query string true "order" Enums(newest,active,score,relevance) -// @Success 200 {object} handler.RespBody{data=schema.SearchListResp} +// @Success 200 {object} handler.RespBody{data=schema.SearchResp} // @Router /answer/api/v1/search [get] func (sc *SearchController) Search(ctx *gin.Context) { dto := schema.SearchDTO{} @@ -35,12 +68,48 @@ func (sc *SearchController) Search(ctx *gin.Context) { return } dto.UserID = middleware.GetLoginUserIDFromContext(ctx) + unit := ctx.ClientIP() + if dto.UserID != "" { + unit = dto.UserID + } + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := sc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionSearch, unit, dto.CaptchaID, dto.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } - resp, total, extra, err := sc.searchService.Search(ctx, &dto) + if !isAdmin { + sc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionSearch, unit) + } + resp, err := sc.searchService.Search(ctx, &dto) + handler.HandleResponse(ctx, err, resp) +} - handler.HandleResponse(ctx, err, schema.SearchListResp{ - Total: total, - SearchResp: resp, - Extra: extra, +// SearchDesc get search description +// @Summary get search description +// @Description get search description +// @Tags Search +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SearchResp} +// @Router /answer/api/v1/search/desc [get] +func (sc *SearchController) SearchDesc(ctx *gin.Context) { + var finder plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + finder = search + return nil }) + resp := &schema.SearchDescResp{} + if finder != nil { + resp.Name = finder.Info().Name.Translate(ctx) + resp.Icon = finder.Description().Icon + resp.Link = finder.Description().Link + } + handler.HandleResponse(ctx, nil, resp) } diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc1782..a336fb242 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -1,47 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "net/http" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" ) -type SiteinfoController struct { - siteInfoService *service.SiteInfoService +type SiteInfoController struct { + siteInfoService siteinfo_common.SiteInfoCommonService } -// NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { - return &SiteinfoController{ +// NewSiteInfoController new site info controller. +func NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController { + return &SiteInfoController{ siteInfoService: siteInfoService, } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json -// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} +// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general - if err != nil { - handler.HandleResponse(ctx, err, resp) - return +func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error(err) + } + resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx) + if err != nil { + log.Error(err) } - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face + resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx) + if err != nil { + log.Error(err) + } + + resp.Login, err = sc.siteInfoService.GetSiteLogin(ctx) + if err != nil { + log.Error(err) + } - handler.HandleResponse(ctx, err, resp) + resp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx) + if err != nil { + log.Error(err) + } + + resp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx) + if err != nil { + log.Error(err) + } + resp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + } + resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx) + if err != nil { + log.Error(err) + } + resp.Write, err = sc.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error(err) + } + if legal, err := sc.siteInfoService.GetSiteLegal(ctx); err == nil { + resp.Legal = &schema.SiteLegalSimpleResp{ExternalContentDisplay: legal.ExternalContentDisplay} + } + + handler.HandleResponse(ctx, nil, resp) +} + +// GetSiteLegalInfo get site legal info +// @Summary get site legal info +// @Description get site legal info +// @Tags site +// @Param info_type query string true "legal information type" Enums(tos, privacy) +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp} +// @Router /answer/api/v1/siteinfo/legal [get] +func (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) { + req := &schema.GetSiteLegalInfoReq{} + if handler.BindAndCheck(ctx, req) { + return + } + siteLegal, err := sc.siteInfoService.GetSiteLegal(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + resp := &schema.GetSiteLegalInfoResp{} + if req.IsTOS() { + resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText + resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText + } else if req.IsPrivacy() { + resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText + resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText + } + handler.HandleResponse(ctx, nil, resp) +} + +// GetManifestJson get manifest.json +func (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) { + favicon := "favicon.ico" + resp := &schema.GetManifestJsonResp{ + ManifestVersion: 3, + Version: constant.Version, + Revision: constant.Revision, + ShortName: "Answer", + Name: "answer.apache.org", + Icons: schema.CreateManifestJsonIcons(favicon), + StartUrl: ".", + Display: "standalone", + ThemeColor: "#000000", + BackgroundColor: "#ffffff", + } + branding, err := sc.siteInfoService.GetSiteBranding(ctx) + if err != nil { + log.Error(err) + } else if len(branding.Favicon) > 0 { + resp.Icons = schema.CreateManifestJsonIcons(branding.Favicon) + } + siteGeneral, err := sc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error(err) + } else { + resp.Name = siteGeneral.Name + resp.ShortName = siteGeneral.Name + } + ctx.JSON(http.StatusOK, resp) } diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go index 601c2c6d1..56555cd88 100644 --- a/internal/controller/tag_controller.go +++ b/internal/controller/tag_controller.go @@ -1,25 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/internal/service/tag" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/tag" + "github.com/apache/answer/internal/service/tag_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // TagController tag controller type TagController struct { - tagService *tag.TagService - rankService *rank.RankService + tagService *tag.TagService + tagCommonService *tag_common.TagCommonService + rankService *rank.RankService } // NewTagController new controller -func NewTagController(tagService *tag.TagService, rankService *rank.RankService) *TagController { - return &TagController{tagService: tagService, rankService: rankService} +func NewTagController( + tagService *tag.TagService, + tagCommonService *tag_common.TagCommonService, + rankService *rank.RankService, +) *TagController { + return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService} } // SearchTagLike get tag list @@ -29,21 +56,39 @@ func NewTagController(tagService *tag.TagService, rankService *rank.RankService) // @Produce json // @Security ApiKeyAuth // @Param tag query string false "tag" -// @Success 200 {object} handler.RespBody{data=[]schema.GetTagResp} +// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp} // @Router /answer/api/v1/question/tags [get] func (tc *TagController) SearchTagLike(ctx *gin.Context) { req := &schema.SearchTagLikeReq{} if handler.BindAndCheck(ctx, req) { return } + resp, err := tc.tagCommonService.SearchTagLike(ctx, req) + handler.HandleResponse(ctx, err, resp) +} - resp, err := tc.tagService.SearchTagLike(ctx, req) +// GetTagsBySlugName get tags list +// @Summary get tags list +// @Description get tags list by slug name +// @Tags Tag +// @Produce json +// @Param tags query []string false "string collection" collectionFormat(csv) +// @Success 200 {object} handler.RespBody{data=[]schema.GetTagBasicResp} +// @Router /answer/api/v1/tags [get] +func (tc *TagController) GetTagsBySlugName(ctx *gin.Context) { + req := &schema.SearchTagsBySlugName{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := tc.tagService.GetTagsBySlugName(ctx, req) handler.HandleResponse(ctx, err, resp) } // RemoveTag delete tag // @Summary delete tag // @Description delete tag +// @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json @@ -57,18 +102,56 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagDeleteRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) return } - - err := tc.tagService.RemoveTag(ctx, req.TagID) + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + err = tc.tagService.RemoveTag(ctx, req) handler.HandleResponse(ctx, err, nil) } +// AddTag add tag +// @Summary add tag +// @Description add tag +// @Security ApiKeyAuth +// @Tags Tag +// @Accept json +// @Produce json +// @Param data body schema.AddTagReq true "tag" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/tag [post] +func (tc *TagController) AddTag(ctx *gin.Context) { + req := &schema.AddTagReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.TagAdd, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + resp, err := tc.tagCommonService.AddTag(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + // UpdateTag update tag // @Summary update tag // @Description update tag +// @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json @@ -82,12 +165,58 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagEditRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.TagEdit, + permission.TagEditWithoutReview, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + req.NoNeedReview = canList[1] + + err = tc.tagService.UpdateTag(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + } else { + handler.HandleResponse(ctx, err, &schema.UpdateTagResp{WaitForReview: !req.NoNeedReview}) + } +} + +// RecoverTag recover delete tag +// @Summary recover delete tag +// @Description recover delete tag +// @Security ApiKeyAuth +// @Tags Tag +// @Accept json +// @Produce json +// @Param data body schema.RecoverTagReq true "tag" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/tag/recover [post] +func (tc *TagController) RecoverTag(ctx *gin.Context) { + req := &schema.RecoverTagReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.TagUnDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !canList[0] { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) return } - err := tc.tagService.UpdateTag(ctx, req) + err = tc.tagService.RecoverTag(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -108,6 +237,19 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{ + permission.TagEdit, + permission.TagDelete, + permission.TagUnDelete, + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = canList[0] + req.CanDelete = canList[1] + req.CanRecover = canList[2] + req.CanMerge = middleware.GetUserIsAdminModerator(ctx) resp, err := tc.tagService.GetTagInfo(ctx, req) handler.HandleResponse(ctx, err, resp) @@ -133,6 +275,14 @@ func (tc *TagController) GetTagWithPage(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := tc.tagService.GetTagWithPage(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if pager.ValPageOutOfRange(resp.Count, req.Page, req.PageSize) { + handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil) + return + } handler.HandleResponse(ctx, err, resp) } @@ -156,7 +306,7 @@ func (tc *TagController) GetFollowingTags(ctx *gin.Context) { // @Tags Tag // @Produce json // @Param tag_id query int true "tag id" -// @Success 200 {object} handler.RespBody{data=[]schema.GetTagSynonymsResp} +// @Success 200 {object} handler.RespBody{data=schema.GetTagSynonymsResp} // @Router /answer/api/v1/tag/synonyms [get] func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { req := &schema.GetTagSynonymsReq{} @@ -164,6 +314,14 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { return } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + req.CanEdit = can + resp, err := tc.tagService.GetTagSynonyms(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -171,6 +329,7 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) { // UpdateTagSynonym update tag // @Summary update tag // @Description update tag +// @Security ApiKeyAuth // @Tags Tag // @Accept json // @Produce json @@ -184,11 +343,44 @@ func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) { } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagSynonymRank); err != nil || !can { - handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition)) + can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "") + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + err = tc.tagService.UpdateTagSynonym(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// MergeTag merge tag +// @Summary merge tag +// @Description merge tag +// @Security ApiKeyAuth +// @Tags Tag +// @Accept json +// @Produce json +// @Param data body schema.AddTagReq true "tag" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/tag/merge [post] +func (tc *TagController) MergeTag(ctx *gin.Context) { + req := &schema.MergeTagReq{} + if handler.BindAndCheck(ctx, req) { return } - err := tc.tagService.UpdateTagSynonym(ctx, req) + isAdminModerator := middleware.GetUserIsAdminModerator(ctx) + if !isAdminModerator { + handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil) + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + err := tc.tagService.MergeTag(ctx, req) + handler.HandleResponse(ctx, err, nil) } diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go new file mode 100644 index 000000000..83cd58c32 --- /dev/null +++ b/internal/controller/template_controller.go @@ -0,0 +1,681 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "encoding/json" + "fmt" + "html/template" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/event_queue" + "github.com/apache/answer/plugin" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + templaterender "github.com/apache/answer/internal/controller/template_render" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/ui" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +var SiteUrl = "" + +type TemplateController struct { + scriptPath []string + cssPath string + templateRenderController *templaterender.TemplateRenderController + siteInfoService siteinfo_common.SiteInfoCommonService + eventQueueService event_queue.EventQueueService + userService *content.UserService + questionService *content.QuestionService +} + +// NewTemplateController new controller +func NewTemplateController( + templateRenderController *templaterender.TemplateRenderController, + siteInfoService siteinfo_common.SiteInfoCommonService, + eventQueueService event_queue.EventQueueService, + userService *content.UserService, + questionService *content.QuestionService, +) *TemplateController { + script, css := GetStyle() + return &TemplateController{ + scriptPath: script, + cssPath: css, + templateRenderController: templateRenderController, + siteInfoService: siteInfoService, + eventQueueService: eventQueueService, + userService: userService, + questionService: questionService, + } +} +func GetStyle() (script []string, css string) { + file, err := ui.Build.ReadFile("build/index.html") + if err != nil { + return + } + scriptRegexp := regexp.MustCompile(``) + scriptData := scriptRegexp.FindAllStringSubmatch(string(file), -1) + for _, s := range scriptData { + if len(s) == 2 { + script = append(script, s[1]) + } + } + + cssRegexp := regexp.MustCompile(``) + cssListData := cssRegexp.FindStringSubmatch(string(file)) + if len(cssListData) == 2 { + css = cssListData[1] + } + return +} +func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp { + var err error + resp := &schema.TemplateSiteInfoResp{} + resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error(err) + } + SiteUrl = resp.General.SiteUrl + resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx) + if err != nil { + log.Error(err) + } + + resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx) + if err != nil { + log.Error(err) + } + + resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + } + + resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx) + if err != nil { + log.Error(err) + } + resp.Year = fmt.Sprintf("%d", time.Now().Year()) + return resp +} + +// Index question list +func (tc *TemplateController) Index(ctx *gin.Context) { + req := &schema.QuestionPageReq{ + OrderCond: "newest", + } + if handler.BindAndCheck(ctx, req) { + return + } + + var page = req.Page + + data, count, err := tc.templateRenderController.Index(ctx, req) + if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { + tc.Page404(ctx) + return + } + + hotQuestionReq := &schema.QuestionPageReq{ + Page: 1, + PageSize: 6, + OrderCond: "hot", + InDays: 7, + } + hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) + + siteInfo := tc.SiteInfo(ctx) + siteInfo.Canonical = siteInfo.General.SiteUrl + + UrlUseTitle := false + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { + UrlUseTitle = true + } + siteInfo.Title = "" + tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ + "data": data, + "useTitle": UrlUseTitle, + "page": templaterender.Paginator(page, req.PageSize, count), + "path": "questions", + "hotQuestion": hotQuestion, + }) +} + +func (tc *TemplateController) QuestionList(ctx *gin.Context) { + req := &schema.QuestionPageReq{ + OrderCond: "newest", + } + if handler.BindAndCheck(ctx, req) { + return + } + var page = req.Page + data, count, err := tc.templateRenderController.Index(ctx, req) + if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) { + tc.Page404(ctx) + return + } + + hotQuestionReq := &schema.QuestionPageReq{ + Page: 1, + PageSize: 6, + OrderCond: "hot", + InDays: 7, + } + hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq) + + siteInfo := tc.SiteInfo(ctx) + siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl) + if page > 1 { + siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page) + } + + UrlUseTitle := false + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { + UrlUseTitle = true + } + siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLang(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) + tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{ + "data": data, + "useTitle": UrlUseTitle, + "page": templaterender.Paginator(page, req.PageSize, count), + "hotQuestion": hotQuestion, + }) +} + +func (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) { + questionID := ctx.Param("id") + title := ctx.Param("title") + answerID := uid.DeShortID(title) + titleIsAnswerID := false + needChangeShortID := false + + objectType, err := obj.GetObjectTypeStrByObjectID(answerID) + if err == nil && objectType == constant.AnswerObjectType { + titleIsAnswerID = true + } + + siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx) + if err != nil { + return false, "" + } + isShortID := uid.IsShortID(questionID) + if siteSeo.IsShortLink() { + if !isShortID { + questionID = uid.EnShortID(questionID) + needChangeShortID = true + } + if titleIsAnswerID { + answerID = uid.EnShortID(answerID) + } + } else { + if isShortID { + needChangeShortID = true + questionID = uid.DeShortID(questionID) + } + if titleIsAnswerID { + answerID = uid.DeShortID(answerID) + } + } + + if _, err := tc.templateRenderController.AnswerDetail(ctx, answerID); err != nil { + answerID = "" + titleIsAnswerID = false + } + + url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, questionID) + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { + if len(ctx.Request.URL.Query()) > 0 { + url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) + } + if needChangeShortID { + return true, url + } + //not have title + if titleIsAnswerID || len(title) == 0 { + return false, "" + } + + return true, url + } else { + + detail, err := tc.templateRenderController.QuestionDetail(ctx, questionID) + if err != nil { + tc.Page404(ctx) + return + } + url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title)) + if titleIsAnswerID { + url = fmt.Sprintf("%s/%s", url, answerID) + } + + if len(ctx.Request.URL.Query()) > 0 { + url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery) + } + //have title + if len(title) > 0 && !titleIsAnswerID && correctTitle { + if needChangeShortID { + return true, url + } + return false, "" + } + return true, url + } +} + +// QuestionInfo question and answers info +func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { + id := ctx.Param("id") + title := ctx.Param("title") + answerid := ctx.Param("answerid") + shareUsername := ctx.Query("share") + if checker.IsQuestionsIgnorePath(id) { + // if id == "ask" { + file, err := ui.Build.ReadFile("build/index.html") + if err != nil { + log.Error(err) + tc.Page404(ctx) + return + } + ctx.Header("content-type", "text/html;charset=utf-8") + ctx.String(http.StatusOK, string(file)) + return + } + + correctTitle := false + + detail, err := tc.templateRenderController.QuestionDetail(ctx, id) + if err != nil { + tc.Page404(ctx) + return + } + if len(shareUsername) > 0 { + userInfo, err := tc.userService.GetOtherUserInfoByUsername( + ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername}) + if err == nil { + tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID). + QID(id, detail.UserID).AID(answerid, "")) + } + } + encodeTitle := htmltext.UrlTitle(detail.Title) + if encodeTitle == title { + correctTitle = true + } + + siteInfo := tc.SiteInfo(ctx) + jump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle) + if jump { + ctx.Redirect(http.StatusFound, jumpurl) + return + } + + // answers + answerReq := &schema.AnswerListReq{ + QuestionID: id, + Order: "", + Page: 1, + PageSize: 999, + UserID: "", + } + answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq) + if err != nil { + tc.Page404(ctx) + return + } + + // comments + objectIDs := []string{uid.DeShortID(id)} + for _, answer := range answers { + answerID := uid.DeShortID(answer.ID) + objectIDs = append(objectIDs, answerID) + } + comments, err := tc.templateRenderController.CommentList(ctx, objectIDs) + if err != nil { + tc.Page404(ctx) + return + } + + UrlUseTitle := false + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { + UrlUseTitle = true + } + + //related question + userID := middleware.GetLoginUserIDFromContext(ctx) + relatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID) + + siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle) + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID { + siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id) + } + jsonLD := &schema.QAPageJsonLD{} + jsonLD.Context = "https://schema.org" + jsonLD.Type = "QAPage" + jsonLD.MainEntity.Type = "Question" + jsonLD.MainEntity.Name = detail.Title + jsonLD.MainEntity.Text = detail.HTML + jsonLD.MainEntity.AnswerCount = int(answerCount) + jsonLD.MainEntity.UpvoteCount = detail.VoteCount + jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0) + jsonLD.MainEntity.Author.Type = "Person" + jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName + jsonLD.MainEntity.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, detail.UserInfo.Username) + answerList := make([]*schema.SuggestedAnswerItem, 0) + for _, answer := range answers { + if answer.Accepted == schema.AnswerAcceptedEnable { + acceptedAnswerItem := &schema.AcceptedAnswerItem{} + acceptedAnswerItem.Type = "Answer" + acceptedAnswerItem.Text = answer.HTML + acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0) + acceptedAnswerItem.UpvoteCount = answer.VoteCount + acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) + acceptedAnswerItem.Author.Type = "Person" + acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName + acceptedAnswerItem.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) + jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem + } else { + item := &schema.SuggestedAnswerItem{} + item.Type = "Answer" + item.Text = answer.HTML + item.DateCreated = time.Unix(answer.CreateTime, 0) + item.UpvoteCount = answer.VoteCount + item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID) + item.Author.Type = "Person" + item.Author.Name = answer.UserInfo.DisplayName + item.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username) + answerList = append(answerList, item) + } + } + jsonLD.MainEntity.SuggestedAnswer = answerList + jsonLDStr, err := json.Marshal(jsonLD) + if err == nil { + siteInfo.JsonLD = `` + } + + siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240) + tags := make([]string, 0) + for _, tag := range detail.Tags { + tags = append(tags, tag.DisplayName) + } + siteInfo.Keywords = strings.Replace(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",", -1) + siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name) + tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{ + "id": id, + "answerid": answerid, + "detail": detail, + "answers": answers, + "comments": comments, + "noindex": detail.Show == entity.QuestionHide, + "useTitle": UrlUseTitle, + "relatedQuestion": relatedQuestion, + }) +} + +// TagList tags list +func (tc *TemplateController) TagList(ctx *gin.Context) { + req := &schema.GetTagWithPageReq{ + PageSize: constant.DefaultPageSize, + Page: 1, + } + if handler.BindAndCheck(ctx, req) { + return + } + data, err := tc.templateRenderController.TagList(ctx, req) + if err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) { + tc.Page404(ctx) + return + } + page := templaterender.Paginator(req.Page, req.PageSize, data.Count) + + siteInfo := tc.SiteInfo(ctx) + siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl) + if req.Page > 1 { + siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page) + } + siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLang(ctx), constant.TagsListTitleTrKey), siteInfo.General.Name) + tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{ + "page": page, + "data": data, + }) +} + +// TagInfo taginfo +func (tc *TemplateController) TagInfo(ctx *gin.Context) { + tag := ctx.Param("tag") + req := &schema.GetTamplateTagInfoReq{} + if handler.BindAndCheck(ctx, req) { + tc.Page404(ctx) + return + } + nowPage := req.Page + req.Name = tag + tagInfo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req) + if err != nil { + tc.Page404(ctx) + return + } + page := templaterender.Paginator(nowPage, req.PageSize, questionCount) + + siteInfo := tc.SiteInfo(ctx) + siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag) + if req.Page > 1 { + siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page) + } + siteInfo.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240) + if len(tagInfo.ParsedText) == 0 { + siteInfo.Description = translator.Tr(handler.GetLang(ctx), constant.TagHasNoDescription) + } + siteInfo.Keywords = tagInfo.DisplayName + + UrlUseTitle := false + if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID { + UrlUseTitle = true + } + siteInfo.Title = fmt.Sprintf("'%s' %s - %s", tagInfo.DisplayName, translator.Tr(handler.GetLang(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name) + tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{ + "tag": tagInfo, + "questionList": questionList, + "questionCount": questionCount, + "useTitle": UrlUseTitle, + "page": page, + }) +} + +// UserInfo user info +func (tc *TemplateController) UserInfo(ctx *gin.Context) { + username := ctx.Param("username") + if username == "" { + tc.Page404(ctx) + return + } + + exist := checker.IsUsersIgnorePath(username) + if exist { + file, err := ui.Build.ReadFile("build/index.html") + if err != nil { + log.Error(err) + tc.Page404(ctx) + return + } + ctx.Header("content-type", "text/html;charset=utf-8") + ctx.String(http.StatusOK, string(file)) + return + } + req := &schema.GetOtherUserInfoByUsernameReq{} + req.Username = username + userinfo, err := tc.templateRenderController.UserInfo(ctx, req) + if err != nil { + tc.Page404(ctx) + return + } + + questionList, answerList, err := tc.questionService.SearchUserTopList(ctx, req.Username, "") + if err != nil { + tc.Page404(ctx) + return + } + + siteInfo := tc.SiteInfo(ctx) + siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username) + siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name) + tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{ + "userinfo": userinfo, + "bio": template.HTML(userinfo.BioHTML), + "topQuestions": questionList, + "topAnswers": answerList, + }) + +} + +func (tc *TemplateController) Page404(ctx *gin.Context) { + tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{}) +} + +func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) { + var ( + prefix = "" + cssPath = "" + scriptPath = make([]string, len(tc.scriptPath)) + ) + + _ = plugin.CallCDN(func(fn plugin.CDN) error { + prefix = fn.GetStaticPrefix() + return nil + }) + + if prefix != "" { + if prefix[len(prefix)-1:] == "/" { + prefix = strings.TrimSuffix(prefix, "/") + } + cssPath = prefix + tc.cssPath + for i, path := range tc.scriptPath { + scriptPath[i] = prefix + path + } + } else { + cssPath = tc.cssPath + scriptPath = tc.scriptPath + } + + data["siteinfo"] = siteInfo + data["baseURL"] = "" + if parsedUrl, err := url.Parse(siteInfo.General.SiteUrl); err == nil { + data["baseURL"] = parsedUrl.Path + } + data["scriptPath"] = scriptPath + data["cssPath"] = cssPath + data["keywords"] = siteInfo.Keywords + if siteInfo.Description == "" { + siteInfo.Description = siteInfo.General.Description + } + data["title"] = siteInfo.Title + if siteInfo.Title == "" { + data["title"] = siteInfo.General.Name + } + data["description"] = siteInfo.Description + data["language"] = handler.GetLang(ctx) + data["timezone"] = siteInfo.Interface.TimeZone + language := strings.Replace(siteInfo.Interface.Language, "_", "-", -1) + data["lang"] = language + data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead + data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader + data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter + data["Version"] = constant.Version + data["Revision"] = constant.Revision + _, ok := data["path"] + if !ok { + data["path"] = "" + } + ctx.Header("X-Frame-Options", "DENY") + ctx.HTML(code, tpl, data) +} + +func (tc *TemplateController) OpenSearch(ctx *gin.Context) { + if tc.checkPrivateMode(ctx) { + tc.Page404(ctx) + return + } + tc.templateRenderController.OpenSearch(ctx) +} + +func (tc *TemplateController) Sitemap(ctx *gin.Context) { + if tc.checkPrivateMode(ctx) { + tc.Page404(ctx) + return + } + tc.templateRenderController.Sitemap(ctx) +} + +func (tc *TemplateController) SitemapPage(ctx *gin.Context) { + if tc.checkPrivateMode(ctx) { + tc.Page404(ctx) + return + } + page := 0 + pageParam := ctx.Param("page") + pageRegexp := regexp.MustCompile(`question-(.*).xml`) + pageStr := pageRegexp.FindStringSubmatch(pageParam) + if len(pageStr) != 2 { + tc.Page404(ctx) + return + } + page = converter.StringToInt(pageStr[1]) + if page == 0 { + tc.Page404(ctx) + return + } + err := tc.templateRenderController.SitemapPage(ctx, page) + if err != nil { + tc.Page404(ctx) + return + } +} + +func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool { + resp, err := tc.siteInfoService.GetSiteLogin(ctx) + if err != nil { + log.Error(err) + return false + } + if resp.LoginRequired { + return true + } + return false +} diff --git a/internal/controller/template_render/answer.go b/internal/controller/template_render/answer.go new file mode 100644 index 000000000..f12497e7f --- /dev/null +++ b/internal/controller/template_render/answer.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "context" + + "github.com/apache/answer/internal/schema" +) + +func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) { + return t.answerService.SearchList(ctx, req) +} + +func (t *TemplateRenderController) AnswerDetail(ctx context.Context, id string) (*schema.AnswerInfo, error) { + return t.answerService.GetDetail(ctx, id) +} diff --git a/internal/controller/template_render/comment.go b/internal/controller/template_render/comment.go new file mode 100644 index 000000000..67efd2129 --- /dev/null +++ b/internal/controller/template_render/comment.go @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "context" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/schema" +) + +func (t *TemplateRenderController) CommentList( + ctx context.Context, + objectIDs []string, +) ( + comments map[string][]*schema.GetCommentResp, + err error, +) { + + comments = make(map[string][]*schema.GetCommentResp, len(objectIDs)) + + for _, objectID := range objectIDs { + var ( + req = &schema.GetCommentWithPageReq{ + Page: 1, + PageSize: 3, + ObjectID: objectID, + QueryCond: "vote", + UserID: "", + } + pageModel *pager.PageModel + ) + pageModel, err = t.commentService.GetCommentWithPage(ctx, req) + if err != nil { + return + } + li := pageModel.List + comments[objectID] = li.([]*schema.GetCommentResp) + } + return +} diff --git a/internal/controller/template_render/controller.go b/internal/controller/template_render/controller.go new file mode 100644 index 000000000..5f802fa76 --- /dev/null +++ b/internal/controller/template_render/controller.go @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "math" + + "github.com/apache/answer/internal/service/content" + questioncommon "github.com/apache/answer/internal/service/question_common" + + "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/google/wire" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/tag" +) + +// ProviderSetTemplateRenderController is template render controller providers. +var ProviderSetTemplateRenderController = wire.NewSet( + NewTemplateRenderController, +) + +type TemplateRenderController struct { + questionService *content.QuestionService + userService *content.UserService + tagService *tag.TagService + answerService *content.AnswerService + commentService *comment.CommentService + siteInfoService siteinfo_common.SiteInfoCommonService + questionRepo questioncommon.QuestionRepo +} + +func NewTemplateRenderController( + questionService *content.QuestionService, + userService *content.UserService, + tagService *tag.TagService, + answerService *content.AnswerService, + commentService *comment.CommentService, + siteInfoService siteinfo_common.SiteInfoCommonService, + questionRepo questioncommon.QuestionRepo, +) *TemplateRenderController { + return &TemplateRenderController{ + questionService: questionService, + userService: userService, + tagService: tagService, + answerService: answerService, + commentService: commentService, + questionRepo: questionRepo, + siteInfoService: siteInfoService, + } +} + +// Paginator page +// page : now page +// pageSize : Number per page +// nums : Total +// Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned +func Paginator(page, pageSize int, nums int64) *schema.Paginator { + if pageSize == 0 { + pageSize = 10 + } + + var prevpage int //Previous page address + var nextpage int //Address on the last page + //Generate the total number of pages based on the total number of nums and the number of prepage pages + totalpages := int(math.Ceil(float64(nums) / float64(pageSize))) //Total number of Pages + if page > totalpages { + page = totalpages + } + if page <= 0 { + page = 1 + } + var pages []int + switch { + case page >= totalpages-5 && totalpages > 5: //The last 5 pages + start := totalpages - 5 + 1 + prevpage = page - 1 + nextpage = int(math.Min(float64(totalpages), float64(page+1))) + pages = make([]int, 5) + for i := range pages { + pages[i] = start + i + } + case page >= 3 && totalpages > 5: + start := page - 3 + 1 + pages = make([]int, 5) + prevpage = page - 3 + for i := range pages { + pages[i] = start + i + } + prevpage = page - 1 + nextpage = page + 1 + default: + pages = make([]int, int(math.Min(5, float64(totalpages)))) + for i := range pages { + pages[i] = i + 1 + } + prevpage = int(math.Max(float64(1), float64(page-1))) + nextpage = page + 1 + } + paginator := &schema.Paginator{} + paginator.Pages = pages + paginator.Totalpages = totalpages + paginator.Prevpage = prevpage + paginator.Nextpage = nextpage + paginator.Currpage = page + return paginator +} diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go new file mode 100644 index 000000000..fa1d48378 --- /dev/null +++ b/internal/controller/template_render/question.go @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "html/template" + "math" + "net/http" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/schema" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionPageReq) ([]*schema.QuestionPageResp, int64, error) { + return t.questionService.GetQuestionPage(ctx, req) +} + +func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfoResp, err error) { + return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{}) +} + +func (t *TemplateRenderController) Sitemap(ctx *gin.Context) { + general, err := t.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error("get site general failed:", err) + return + } + siteInfo, err := t.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error("get site GetSiteSeo failed:", err) + return + } + + questions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize) + if err != nil { + log.Errorf("get sitemap questions failed: %s", err) + return + } + + ctx.Header("Content-Type", "application/xml") + if len(questions) < constant.SitemapMaxSize { + ctx.HTML( + http.StatusOK, "sitemap.xml", gin.H{ + "xmlHeader": template.HTML(``), + "list": questions, + "general": general, + "hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID, + }, + ) + return + } + + questionNum, err := t.questionRepo.GetQuestionCount(ctx) + if err != nil { + log.Error("GetQuestionCount error", err) + return + } + var pageList []int + totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) + for i := 1; i <= totalPages; i++ { + pageList = append(pageList, i) + } + ctx.HTML( + http.StatusOK, "sitemap-list.xml", gin.H{ + "xmlHeader": template.HTML(``), + "page": pageList, + "general": general, + }, + ) +} + +func (t *TemplateRenderController) OpenSearch(ctx *gin.Context) { + general, err := t.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error("get site general failed:", err) + return + } + + favicon := general.SiteUrl + "/favicon.ico" + branding, err := t.siteInfoService.GetSiteBranding(ctx) + if err == nil && len(branding.Favicon) > 0 { + favicon = branding.Favicon + } + + ctx.Header("Content-Type", "application/xml") + ctx.HTML( + http.StatusOK, "opensearch.xml", gin.H{ + "general": general, + "favicon": favicon, + }, + ) +} + +func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error { + general, err := t.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error("get site general failed:", err) + return err + } + siteInfo, err := t.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error("get site GetSiteSeo failed:", err) + return err + } + + questions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize) + if err != nil { + log.Errorf("get sitemap questions failed: %s", err) + return err + } + ctx.Header("Content-Type", "application/xml") + ctx.HTML( + http.StatusOK, "sitemap.xml", gin.H{ + "xmlHeader": template.HTML(``), + "list": questions, + "general": general, + "hastitle": siteInfo.Permalink == constant.PermalinkQuestionIDAndTitle || + siteInfo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID, + }, + ) + return nil +} diff --git a/internal/controller/template_render/tags.go b/internal/controller/template_render/tags.go new file mode 100644 index 000000000..6a6dacc9d --- /dev/null +++ b/internal/controller/template_render/tags.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/schema" + "github.com/jinzhu/copier" + "golang.org/x/net/context" +) + +func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) { + resp, err = q.tagService.GetTagWithPage(ctx, req) + if err != nil { + return + } + return +} + +func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionPageResp, questionCount int64, err error) { + dto := &schema.GetTagInfoReq{} + _ = copier.Copy(dto, req) + resp, err = q.tagService.GetTagInfo(ctx, dto) + if err != nil { + return + } + searchQuestion := &schema.QuestionPageReq{} + searchQuestion.Page = req.Page + searchQuestion.PageSize = req.PageSize + searchQuestion.OrderCond = "newest" + searchQuestion.Tag = req.Name + searchQuestion.LoginUserID = req.UserID + questionList, questionCount, err = q.questionService.GetQuestionPage(ctx, searchQuestion) + if err != nil { + return + } + return resp, questionList, questionCount, err +} diff --git a/internal/controller/template_render/userinfo.go b/internal/controller/template_render/userinfo.go new file mode 100644 index 000000000..1734f65c1 --- /dev/null +++ b/internal/controller/template_render/userinfo.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package templaterender + +import ( + "github.com/apache/answer/internal/schema" + "golang.org/x/net/context" +) + +func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) { + return q.userService.GetOtherUserInfoByUsername(ctx, req) +} diff --git a/internal/controller/upload_controller.go b/internal/controller/upload_controller.go new file mode 100644 index 000000000..337cf83d4 --- /dev/null +++ b/internal/controller/upload_controller.go @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/uploader" + "github.com/apache/answer/pkg/converter" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +const ( + // file is uploaded by markdown(or something else) editor + fileFromPost = "post" + // file is used to upload the post attachment + fileFromPostAttachment = "post_attachment" + // file is used to change the user's avatar + fileFromAvatar = "avatar" + // file is logo/icon images + fileFromBranding = "branding" +) + +// UploadController upload controller +type UploadController struct { + uploaderService uploader.UploaderService +} + +// NewUploadController new controller +func NewUploadController(uploaderService uploader.UploaderService) *UploadController { + return &UploadController{ + uploaderService: uploaderService, + } +} + +// UploadFile upload file +// @Summary upload file +// @Description upload file +// @Tags Upload +// @Accept multipart/form-data +// @Security ApiKeyAuth +// @Param source formData string true "identify the source of the file upload" Enums(post, post_attachment, avatar, branding) +// @Param file formData file true "file" +// @Success 200 {object} handler.RespBody{data=string} +// @Router /answer/api/v1/file [post] +func (uc *UploadController) UploadFile(ctx *gin.Context) { + var ( + url string + err error + ) + + source := ctx.PostForm("source") + userID := middleware.GetLoginUserIDFromContext(ctx) + switch source { + case fileFromAvatar: + url, err = uc.uploaderService.UploadAvatarFile(ctx, userID) + case fileFromPost: + url, err = uc.uploaderService.UploadPostFile(ctx, userID) + case fileFromBranding: + if !middleware.GetIsAdminFromContext(ctx) { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + url, err = uc.uploaderService.UploadBrandingFile(ctx, userID) + case fileFromPostAttachment: + url, err = uc.uploaderService.UploadPostAttachment(ctx, userID) + default: + handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil) + return + } + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, err, url) +} + +// PostRender render post content +// @Summary render post content +// @Description render post content +// @Tags Upload +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.PostRenderReq true "PostRenderReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/post/render [post] +func (uc *UploadController) PostRender(ctx *gin.Context) { + req := &schema.PostRenderReq{} + if handler.BindAndCheck(ctx, req) { + return + } + handler.HandleResponse(ctx, nil, converter.Markdown2HTML(req.Content)) +} diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 390b72ced..49b9b23c6 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -1,20 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "net/http" - "path" - "strings" - - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" - "github.com/answerdev/answer/internal/service/action" - "github.com/answerdev/answer/internal/service/auth" - "github.com/answerdev/answer/internal/service/export" - "github.com/answerdev/answer/internal/service/uploader" + "net/url" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/user_notification_config" + "github.com/apache/answer/pkg/checker" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -22,42 +44,58 @@ import ( // UserController user controller type UserController struct { - userService *service.UserService - authService *auth.AuthService - actionService *action.CaptchaService - uploaderService *uploader.UploaderService - emailService *export.EmailService + userService *content.UserService + authService *auth.AuthService + actionService *action.CaptchaService + emailService *export.EmailService + siteInfoCommonService siteinfo_common.SiteInfoCommonService + userNotificationConfigService *user_notification_config.UserNotificationConfigService } // NewUserController new controller func NewUserController( authService *auth.AuthService, - userService *service.UserService, + userService *content.UserService, actionService *action.CaptchaService, emailService *export.EmailService, - uploaderService *uploader.UploaderService) *UserController { + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + userNotificationConfigService *user_notification_config.UserNotificationConfigService, +) *UserController { return &UserController{ - authService: authService, - userService: userService, - actionService: actionService, - uploaderService: uploaderService, - emailService: emailService, + authService: authService, + userService: userService, + actionService: actionService, + emailService: emailService, + siteInfoCommonService: siteInfoCommonService, + userNotificationConfigService: userNotificationConfigService, } } -// GetUserInfoByUserID godoc +// GetUserInfoByUserID get user info, if user no login response http code is 200, but user info is null // @Summary GetUserInfoByUserID -// @Description GetUserInfoByUserID +// @Description get user info, if user no login response http code is 200, but user info is null // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} +// @Success 200 {object} handler.RespBody{data=schema.GetCurrentLoginUserInfoResp} // @Router /answer/api/v1/user/info [get] func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) { - userID := middleware.GetLoginUserIDFromContext(ctx) token := middleware.ExtractToken(ctx) - resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userID) + if len(token) == 0 { + handler.HandleResponse(ctx, nil, nil) + return + } + + // if user is no login return null in data + userInfo, _ := uc.authService.GetUserCacheInfo(ctx, token) + if userInfo == nil { + handler.HandleResponse(ctx, nil, nil) + return + } + + resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID) + uc.setVisitCookies(ctx, userInfo.VisitToken, false) handler.HandleResponse(ctx, err, resp) } @@ -77,23 +115,10 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) { return } - resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req.Username) - handler.HandleResponse(ctx, err, resp) -} + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) -// GetUserStatus get user status info -// @Summary get user status info -// @Description get user status info -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} -// @Router /answer/api/v1/user/status [get] -func (uc *UserController) GetUserStatus(ctx *gin.Context) { - userID := middleware.GetLoginUserIDFromContext(ctx) - token := middleware.ExtractToken(ctx) - resp, err := uc.userService.GetUserStatus(ctx, userID, token) + resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req) handler.HandleResponse(ctx, err, resp) } @@ -103,38 +128,46 @@ func (uc *UserController) GetUserStatus(ctx *gin.Context) { // @Tags User // @Accept json // @Produce json -// @Param data body schema.UserEmailLogin true "UserEmailLogin" -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} +// @Param data body schema.UserEmailLoginReq true "UserEmailLogin" +// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/login/email [post] func (uc *UserController) UserEmailLogin(ctx *gin.Context) { - req := &schema.UserEmailLogin{} + req := &schema.UserEmailLoginReq{} if handler.BindAndCheck(ctx, req) { return } - - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) - if !captchaPass { - resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", - Value: "error.object.verification_failed", + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionPassword, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) - return } resp, err := uc.userService.EmailLogin(ctx, req) if err != nil { - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) - resp := schema.UserVerifyEmailErrorResponse{ - Key: "e_mail", - Value: "error.object.email_or_password_incorrect", - } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) + _, _ = uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "e_mail", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields) return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) + if !isAdmin { + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) + } + if resp.Status == constant.UserSuspended { + handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended}) + return + } + uc.setVisitCookies(ctx, resp.VisitToken, true) handler.HandleResponse(ctx, nil, resp) } @@ -152,19 +185,20 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) - if !captchaPass { - resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", - Value: "error.object.verification_failed", + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) - return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) - code, err := uc.userService.RetrievePassWord(ctx, req) - handler.HandleResponse(ctx, err, code) + err := uc.userService.RetrievePassWord(ctx, req) + handler.HandleResponse(ctx, err, nil) } // UseRePassWord godoc @@ -184,19 +218,20 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } - resp, err := uc.userService.UseRePassWord(ctx, req) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) - handler.HandleResponse(ctx, err, resp) + err := uc.userService.UpdatePasswordWhenForgot(ctx, req) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionPassword, ctx.ClientIP()) + handler.HandleResponse(ctx, err, nil) } // UserLogout user logout // @Summary user logout // @Description user logout +// @Security ApiKeyAuth // @Tags User // @Accept json // @Produce json @@ -204,7 +239,14 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { // @Router /answer/api/v1/user/logout [get] func (uc *UserController) UserLogout(ctx *gin.Context) { accessToken := middleware.ExtractToken(ctx) + if len(accessToken) == 0 { + handler.HandleResponse(ctx, nil, nil) + return + } _ = uc.authService.RemoveUserCacheInfo(ctx, accessToken) + _ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken) + visitToken, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey) + _ = uc.authService.RemoveUserVisitCacheInfo(ctx, visitToken) handler.HandleResponse(ctx, nil, nil) } @@ -215,17 +257,52 @@ func (uc *UserController) UserLogout(ctx *gin.Context) { // @Accept json // @Produce json // @Param data body schema.UserRegisterReq true "UserRegisterReq" -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} +// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/register/email [post] func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) { + // check whether site allow register or not + siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations { + handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil) + return + } + req := &schema.UserRegisterReq{} if handler.BindAndCheck(ctx, req) { return } + if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) { + handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil) + return + } req.IP = ctx.ClientIP() + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, req.IP, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } - resp, err := uc.userService.UserRegisterByEmail(ctx, req) - handler.HandleResponse(ctx, err, resp) + resp, errFields, err := uc.userService.UserRegisterByEmail(ctx, req) + if len(errFields) > 0 { + for _, field := range errFields { + field.ErrorMsg = translator. + Tr(handler.GetLang(ctx), field.ErrorMsg) + } + handler.HandleResponse(ctx, err, errFields) + } else { + handler.HandleResponse(ctx, err, resp) + } } // UserVerifyEmail godoc @@ -235,7 +312,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) { // @Accept json // @Produce json // @Param code query string true "code" default() -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} +// @Success 200 {object} handler.RespBody{data=schema.UserLoginResp} // @Router /answer/api/v1/user/email/verification [post] func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { req := &schema.UserVerifyEmailReq{} @@ -245,8 +322,8 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } @@ -256,7 +333,7 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -281,20 +358,19 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } - - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP(), - req.CaptchaID, req.CaptchaCode) - if !captchaPass { - resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", - Value: "error.object.verification_failed", + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) - - return } - uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) handler.HandleResponse(ctx, err, nil) } @@ -306,15 +382,33 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest" +// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/user/password [put] func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { - req := &schema.UserModifyPassWordRequest{} + req := &schema.UserModifyPasswordReq{} if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.AccessToken = middleware.ExtractToken(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, + req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + _, err := uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID) + if err != nil { + log.Error(err) + } + } oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req) if err != nil { @@ -322,25 +416,26 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { return } if !oldPassVerification { - resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", - Value: "error.object.old_password_verification_failed", - } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "old_pass", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields) return } - if req.OldPass == req.Pass { - resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", - Value: "error.object.new_password_same_as_previous_setting", - } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) + if req.OldPass == req.Pass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "pass", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields) return } - err = uc.userService.UserModifyPassWord(ctx, req) + err = uc.userService.UserModifyPassword(ctx, req) + if err == nil { + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, req.UserID) + } handler.HandleResponse(ctx, err, nil) } @@ -360,67 +455,34 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) - err := uc.userService.UpdateInfo(ctx, req) - handler.HandleResponse(ctx, err, nil) -} - -// UploadUserAvatar godoc -// @Summary UserUpdateInfo -// @Description UserUpdateInfo -// @Tags User -// @Accept multipart/form-data -// @Security ApiKeyAuth -// @Param file formData file true "file" -// @Success 200 {object} handler.RespBody{data=string} -// @Router /answer/api/v1/user/avatar/upload [post] -func (uc *UserController) UploadUserAvatar(ctx *gin.Context) { - // max size - ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) - _, header, err := ctx.Request.FormFile("file") - if err != nil { - log.Error(err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - fileExt := strings.ToLower(path.Ext(header.Filename)) - if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" { - log.Errorf("upload file format is not supported: %s", fileExt) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + errFields, err := uc.userService.UpdateInfo(ctx, req) + for _, field := range errFields { + field.ErrorMsg = translator.Tr(handler.GetLang(ctx), field.ErrorMsg) } - - url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt) - handler.HandleResponse(ctx, err, url) + handler.HandleResponse(ctx, err, errFields) } -// UploadUserPostFile godoc -// @Summary upload user post file -// @Description upload user post file +// UserUpdateInterface update user interface config +// @Summary UserUpdateInterface update user interface config +// @Description UserUpdateInterface update user interface config // @Tags User -// @Accept multipart/form-data +// @Accept json +// @Produce json // @Security ApiKeyAuth -// @Param file formData file true "file" -// @Success 200 {object} handler.RespBody{data=string} -// @Router /answer/api/v1/user/post/file [post] -func (uc *UserController) UploadUserPostFile(ctx *gin.Context) { - // max size - ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) - _, header, err := ctx.Request.FormFile("file") - if err != nil { - log.Error(err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) - return - } - fileExt := strings.ToLower(path.Ext(header.Filename)) - if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" { - log.Errorf("upload file format is not supported: %s", fileExt) - handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil) +// @Param Authorization header string true "access-token" +// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/user/interface [put] +func (uc *UserController) UserUpdateInterface(ctx *gin.Context) { + req := &schema.UpdateUserInterfaceRequest{} + if handler.BindAndCheck(ctx, req) { return } - - url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt) - handler.HandleResponse(ctx, err, url) + req.UserId = middleware.GetLoginUserIDFromContext(ctx) + err := uc.userService.UserUpdateInterface(ctx, req) + handler.HandleResponse(ctx, err, nil) } // ActionRecord godoc @@ -436,36 +498,63 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.Ip = ctx.ClientIP() + userinfo := middleware.GetUserInfoFromContext(ctx) + if userinfo != nil { + req.UserID = userinfo.UserID + } + req.IP = ctx.ClientIP() + resp := &schema.ActionRecordResp{} + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if isAdmin { + resp.Verify = false + handler.HandleResponse(ctx, nil, resp) + } else { + resp, err := uc.actionService.ActionRecord(ctx, req) + handler.HandleResponse(ctx, err, resp) + } - resp, err := uc.actionService.ActionRecord(ctx, req) +} + +// GetUserNotificationConfig get user's notification config +// @Summary get user's notification config +// @Description get user's notification config +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} handler.RespBody{data=schema.GetUserNotificationConfigResp} +// @Router /answer/api/v1/user/notification/config [post] +func (uc *UserController) GetUserNotificationConfig(ctx *gin.Context) { + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := uc.userNotificationConfigService.GetUserNotificationConfig(ctx, userID) handler.HandleResponse(ctx, err, resp) } -// UserNoticeSet godoc -// @Summary UserNoticeSet -// @Description UserNoticeSet +// UpdateUserNotificationConfig update user's notification config +// @Summary update user's notification config +// @Description update user's notification config // @Tags User // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.UserNoticeSetRequest true "UserNoticeSetRequest" -// @Success 200 {object} handler.RespBody{data=schema.UserNoticeSetResp} -// @Router /answer/api/v1/user/notice/set [post] -func (uc *UserController) UserNoticeSet(ctx *gin.Context) { - req := &schema.UserNoticeSetRequest{} +// @Param data body schema.UpdateUserNotificationConfigReq true "UpdateUserNotificationConfigReq" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/user/notification/config [put] +func (uc *UserController) UpdateUserNotificationConfig(ctx *gin.Context) { + req := &schema.UpdateUserNotificationConfigReq{} if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) - resp, err := uc.userService.UserNoticeSet(ctx, req.UserId, req.NoticeSwitch) - handler.HandleResponse(ctx, err, resp) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + err := uc.userNotificationConfigService.UpdateUserNotificationConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) } // UserChangeEmailSendCode send email to the user email then change their email // @Summary send email to the user email then change their email // @Description send email to the user email then change their email +// @Security ApiKeyAuth // @Tags User // @Accept json // @Produce json @@ -478,8 +567,46 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + // If the user is not logged in, the api cannot be used. + // If the user email is not verified, that also can use this api to modify the email. + if len(req.UserID) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } + // check whether email allow register or not + siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) { + handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil) + return + } + isAdmin := middleware.GetUserIsAdminModerator(ctx) + + if !isAdmin { + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionEditUserinfo, req.UserID, req.CaptchaID, req.CaptchaCode) + uc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEditUserinfo, req.UserID) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + resp, err := uc.userService.UserChangeEmailSendCode(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, resp) + return + } + if !isAdmin { + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEditUserinfo, ctx.ClientIP()) + } - err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -500,11 +627,113 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } - err := uc.userService.UserChangeEmailVerify(ctx, req.Content) + resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content) + uc.actionService.ActionRecordDel(ctx, entity.CaptchaActionEmail, ctx.ClientIP()) + handler.HandleResponse(ctx, err, resp) +} + +// UserRanking get user ranking +// @Summary get user ranking +// @Description get user ranking +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.UserRankingResp} +// @Router /answer/api/v1/user/ranking [get] +func (uc *UserController) UserRanking(ctx *gin.Context) { + resp, err := uc.userService.UserRanking(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// UserStaff get user staff +// @Summary get user staff +// @Description get user staff +// @Tags User +// @Accept json +// @Produce json +// @Param username query string true "username" +// @Param page_size query string true "page_size" +// @Success 200 {object} handler.RespBody{data=schema.GetUserStaffResp} +// @Router /answer/api/v1/user/staff [get] +func (uc *UserController) UserStaff(ctx *gin.Context) { + req := &schema.GetUserStaffReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userService.GetUserStaff(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// UserUnsubscribeNotification unsubscribe notification +// @Summary unsubscribe notification +// @Description unsubscribe notification +// @Tags User +// @Accept json +// @Produce json +// @Param data body schema.UserUnsubscribeNotificationReq true "UserUnsubscribeNotificationReq" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/user/notification/unsubscribe [put] +func (uc *UserController) UserUnsubscribeNotification(ctx *gin.Context) { + req := &schema.UserUnsubscribeNotificationReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) + if len(req.Content) == 0 { + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) + return + } + + err := uc.userService.UserUnsubscribeNotification(ctx, req) handler.HandleResponse(ctx, err, nil) } + +// SearchUserListByName godoc +// @Summary SearchUserListByName +// @Description SearchUserListByName +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param username query string true "username" +// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp} +// @Router /answer/api/v1/user/info/search [get] +func (uc *UserController) SearchUserListByName(ctx *gin.Context) { + req := &schema.GetOtherUserInfoByUsernameReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := uc.userService.SearchUserListByName(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +func (uc *UserController) setVisitCookies(ctx *gin.Context, visitToken string, force bool) { + if !force { + cookie, _ := ctx.Cookie(constant.UserVisitCookiesCacheKey) + // If the cookie is the same as the visitToken, no need to set it again + if cookie == visitToken { + return + } + } + general, err := uc.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general error: %v", err) + return + } + parsedURL, err := url.Parse(general.SiteUrl) + if err != nil { + log.Errorf("parse url error: %v", err) + return + } + ctx.SetCookie(constant.UserVisitCookiesCacheKey, + visitToken, constant.UserVisitCacheTime, "/", parsedURL.Hostname(), true, true) +} diff --git a/internal/controller/user_plugin_controller.go b/internal/controller/user_plugin_controller.go new file mode 100644 index 000000000..310215253 --- /dev/null +++ b/internal/controller/user_plugin_controller.go @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "encoding/json" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/segmentfault/pacman/errors" + "net/http" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/plugin_common" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +// UserPluginController role controller +type UserPluginController struct { + pluginCommonService *plugin_common.PluginCommonService +} + +// NewUserPluginController new controller +func NewUserPluginController(pluginCommonService *plugin_common.PluginCommonService) *UserPluginController { + return &UserPluginController{pluginCommonService: pluginCommonService} +} + +// GetUserPluginList get plugin list that used for user. +// @Summary get plugin list that used for user. +// @Description get plugin list that used for user. +// @Tags UserPlugin +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.GetUserPluginListResp} +// @Router /answer/api/v1/user/plugin/configs [get] +func (pc *UserPluginController) GetUserPluginList(ctx *gin.Context) { + resp := make([]*schema.GetUserPluginListResp, 0) + _ = plugin.CallUserConfig(func(base plugin.UserConfig) error { + info := base.Info() + if plugin.StatusManager.IsEnabled(info.SlugName) { + resp = append(resp, &schema.GetUserPluginListResp{ + Name: info.Name.Translate(ctx), + SlugName: info.SlugName, + }) + } + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} + +// GetUserPluginConfig get user plugin config +// @Summary get user plugin config +// @Description get user plugin config +// @Tags UserPlugin +// @Security ApiKeyAuth +// @Produce json +// @Param plugin_slug_name query string true "plugin_slug_name" +// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp} +// @Router /answer/api/v1/user/plugin/config [get] +func (pc *UserPluginController) GetUserPluginConfig(ctx *gin.Context) { + req := &schema.GetUserPluginConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp := &schema.GetUserPluginConfigResp{} + _ = plugin.CallUserConfig(func(fn plugin.UserConfig) error { + if fn.Info().SlugName != req.PluginSlugName { + return nil + } + info := fn.Info() + resp.Name = info.Name.Translate(ctx) + resp.SlugName = info.SlugName + resp.SetConfigFields(ctx, fn.UserConfigFields()) + return nil + }) + + configValue, err := pc.pluginCommonService.GetUserPluginConfig(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if len(configValue) > 0 { + configValueMapping := make(map[string]any) + _ = json.Unmarshal([]byte(configValue), &configValueMapping) + for _, field := range resp.ConfigFields { + if value, ok := configValueMapping[field.Name]; ok { + field.Value = value + } + } + } + + handler.HandleResponse(ctx, err, resp) +} + +// UpdatePluginUserConfig update user plugin config +// @Summary update user plugin config +// @Description update user plugin config +// @Tags UserPlugin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateUserPluginConfigReq true "UpdatePluginConfigReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/user/plugin/config [put] +func (pc *UserPluginController) UpdatePluginUserConfig(ctx *gin.Context) { + req := &schema.UpdateUserPluginConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + if !plugin.StatusManager.IsEnabled(req.PluginSlugName) { + handler.HandleResponse(ctx, errors.New(http.StatusBadRequest, reason.RequestFormatError), nil) + return + } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + configFields, _ := json.Marshal(req.ConfigFields) + err := plugin.CallUserConfig(func(fn plugin.UserConfig) error { + if fn.Info().SlugName == req.PluginSlugName { + return fn.UserConfigReceiver(req.UserID, configFields) + } + return nil + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + err = pc.pluginCommonService.UpdatePluginUserConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller/vote_controller.go b/internal/controller/vote_controller.go index 7fcdf232c..2e0ee6121 100644 --- a/internal/controller/vote_controller.go +++ b/internal/controller/vote_controller.go @@ -1,22 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package controller import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/middleware" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/pkg/uid" "github.com/gin-gonic/gin" - "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" ) // VoteController activity controller type VoteController struct { - VoteService *service.VoteService + VoteService *content.VoteService + rankService *rank.RankService + actionService *action.CaptchaService } // NewVoteController new controller -func NewVoteController(voteService *service.VoteService) *VoteController { - return &VoteController{VoteService: voteService} +func NewVoteController( + voteService *content.VoteService, + rankService *rank.RankService, + actionService *action.CaptchaService, +) *VoteController { + return &VoteController{ + VoteService: voteService, + rankService: rankService, + actionService: actionService, + } } // VoteUp godoc @@ -34,10 +70,38 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) - dto.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := vc.VoteService.VoteUp(ctx, dto) + req.ObjectID = uid.DeShortID(req.ObjectID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + lang := handler.GetLang(ctx) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank}) + handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) + return + } + + isAdmin := middleware.GetUserIsAdminModerator(ctx) + if !isAdmin { + captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + + if !isAdmin { + vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) + } + resp, err := vc.VoteService.VoteUp(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -60,11 +124,37 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - dto := &schema.VoteDTO{} - _ = copier.Copy(dto, req) + req.ObjectID = uid.DeShortID(req.ObjectID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + isAdmin := middleware.GetUserIsAdminModerator(ctx) - dto.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := vc.VoteService.VoteDown(ctx, dto) + can, needRank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + if !can { + lang := handler.GetLang(ctx) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: needRank}) + handler.HandleResponse(ctx, errors.Forbidden(reason.NoEnoughRankToOperate).WithMsg(msg), nil) + return + } + + if !isAdmin { + captchaPass := vc.actionService.ActionRecordVerifyCaptcha(ctx, entity.CaptchaActionVote, req.UserID, req.CaptchaID, req.CaptchaCode) + if !captchaPass { + errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "captcha_code", + ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed), + }) + handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields) + return + } + } + if !isAdmin { + vc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionVote, req.UserID) + } + resp, err := vc.VoteService.VoteDown(ctx, req) if err != nil { handler.HandleResponse(ctx, err, schema.ErrTypeToast) } else { @@ -72,9 +162,9 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { } } -// UserVotes godoc -// @Summary user's votes -// @Description user's vote +// UserVotes user votes +// @Summary get user personal votes +// @Description get user personal votes // @Tags Activity // @Accept json // @Produce json @@ -85,21 +175,12 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) { // @Router /answer/api/v1/personal/vote/page [get] func (vc *VoteController) UserVotes(ctx *gin.Context) { req := schema.GetVoteWithPageReq{} - req.UserID = middleware.GetLoginUserIDFromContext(ctx) if handler.BindAndCheck(ctx, &req) { return } - if req.Page == 0 { - req.Page = 1 - } - if req.PageSize == 0 { - req.PageSize = 30 - } + + req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, err := vc.VoteService.ListUserVotes(ctx, req) - if err != nil { - handler.HandleResponse(ctx, err, schema.ErrTypeModal) - } else { - handler.HandleResponse(ctx, err, resp) - } + handler.HandleResponse(ctx, err, resp) } diff --git a/internal/controller_admin/badge_controller.go b/internal/controller_admin/badge_controller.go new file mode 100644 index 000000000..e399062c9 --- /dev/null +++ b/internal/controller_admin/badge_controller.go @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/badge" + "github.com/gin-gonic/gin" +) + +type BadgeController struct { + badgeService *badge.BadgeService +} + +func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { + return &BadgeController{ + badgeService: badgeService, + } +} + +// GetBadgeList list all badges by page +// @Summary list all badges by page +// @Description list all badges by page +// @Tags AdminBadge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Param page_size query int false "page size" +// @Param status query string false "badge status" Enums(, active, inactive) +// @Param q query string false "search param" +// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListPagedResp} +// @Router /answer/admin/api/badges [get] +func (b *BadgeController) GetBadgeList(ctx *gin.Context) { + req := &schema.GetBadgeListPagedReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, total, err := b.badgeService.ListPaged(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} + +// UpdateBadgeStatus update badge status +// @Summary update badge status +// @Description update badge status +// @Tags AdminBadge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateBadgeStatusReq true "UpdateBadgeStatusReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/badge/status [put] +func (b *BadgeController) UpdateBadgeStatus(ctx *gin.Context) { + req := &schema.UpdateBadgeStatusReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := b.badgeService.UpdateStatus(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller_admin/controller.go b/internal/controller_admin/controller.go new file mode 100644 index 000000000..ebf32cbfc --- /dev/null +++ b/internal/controller_admin/controller.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import "github.com/google/wire" + +// ProviderSetController is controller providers. +var ProviderSetController = wire.NewSet( + NewUserAdminController, + NewThemeController, + NewSiteInfoController, + NewRoleController, + NewPluginController, + NewBadgeController, +) diff --git a/internal/controller_admin/plugin_controller.go b/internal/controller_admin/plugin_controller.go new file mode 100644 index 000000000..5933f8895 --- /dev/null +++ b/internal/controller_admin/plugin_controller.go @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "encoding/json" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/plugin_common" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +// PluginController role controller +type PluginController struct { + pluginCommonService *plugin_common.PluginCommonService +} + +// NewPluginController new controller +func NewPluginController(pluginCommonService *plugin_common.PluginCommonService) *PluginController { + return &PluginController{pluginCommonService: pluginCommonService} +} + +// GetAllPluginStatus get all plugins status +// @Summary get all plugins status +// @Description get all plugins status +// @Tags Plugin +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp} +// @Router /answer/api/v1/plugin/status [get] +func (pc *PluginController) GetAllPluginStatus(ctx *gin.Context) { + resp := make([]*schema.GetAllPluginStatusResp, 0) + _ = plugin.CallBase(func(base plugin.Base) error { + info := base.Info() + resp = append(resp, &schema.GetAllPluginStatusResp{ + SlugName: info.SlugName, + Enabled: plugin.StatusManager.IsEnabled(info.SlugName), + }) + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} + +// GetPluginList get plugin list +// @Summary get plugin list +// @Description get plugin list +// @Tags AdminPlugin +// @Security ApiKeyAuth +// @Accept json +// @Produce json +// @Param status query string false "status: active/inactive" +// @Param have_config query boolean false "have config" +// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp} +// @Router /answer/admin/api/plugins [get] +func (pc *PluginController) GetPluginList(ctx *gin.Context) { + req := &schema.GetPluginListReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + pluginConfigMapping := make(map[string]bool) + _ = plugin.CallConfig(func(fn plugin.Config) error { + if len(fn.ConfigFields()) > 0 { + pluginConfigMapping[fn.Info().SlugName] = true + } + return nil + }) + + resp := make([]*schema.GetPluginListResp, 0) + _ = plugin.CallBase(func(base plugin.Base) error { + info := base.Info() + resp = append(resp, &schema.GetPluginListResp{ + Name: info.Name.Translate(ctx), + SlugName: info.SlugName, + Description: info.Description.Translate(ctx), + Version: info.Version, + Enabled: plugin.StatusManager.IsEnabled(info.SlugName), + HaveConfig: pluginConfigMapping[info.SlugName], + Link: info.Link, + }) + return nil + }) + + if len(req.Status) > 0 { + resp = pc.filterPluginByStatus(resp, req.Status) + } + if req.HaveConfig { + resp = pc.filterNoConfigPlugin(resp) + } + handler.HandleResponse(ctx, nil, resp) +} + +func (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp { + resp := make([]*schema.GetPluginListResp, 0) + for _, t := range list { + if t.HaveConfig { + resp = append(resp, t) + } + } + return resp +} + +func (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus, +) []*schema.GetPluginListResp { + resp := make([]*schema.GetPluginListResp, 0) + for _, t := range list { + if status == schema.PluginStatusActive && t.Enabled { + resp = append(resp, t) + } else if status == schema.PluginStatusInactive && !t.Enabled { + resp = append(resp, t) + } + } + return resp +} + +// UpdatePluginStatus update plugin status +// @Summary update plugin status +// @Description update plugin status +// @Tags AdminPlugin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdatePluginStatusReq true "UpdatePluginStatusReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/plugin/status [put] +func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) { + req := &schema.UpdatePluginStatusReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled) + err := pc.pluginCommonService.UpdatePluginStatus(ctx) + handler.HandleResponse(ctx, err, nil) +} + +// GetPluginConfig get plugin config +// @Summary get plugin config +// @Description get plugin config +// @Tags AdminPlugin +// @Security ApiKeyAuth +// @Produce json +// @Param plugin_slug_name query string true "plugin_slug_name" +// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp} +// @Router /answer/admin/api/plugin/config [get] +func (pc *PluginController) GetPluginConfig(ctx *gin.Context) { + req := &schema.GetPluginConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp := &schema.GetPluginConfigResp{} + _ = plugin.CallBase(func(base plugin.Base) error { + if base.Info().SlugName != req.PluginSlugName { + return nil + } + info := base.Info() + resp.Name = info.Name.Translate(ctx) + resp.SlugName = info.SlugName + resp.Description = info.Description.Translate(ctx) + resp.Version = info.Version + return nil + }) + + _ = plugin.CallConfig(func(fn plugin.Config) error { + if fn.Info().SlugName != req.PluginSlugName { + return nil + } + resp.SetConfigFields(ctx, fn.ConfigFields()) + return nil + }) + handler.HandleResponse(ctx, nil, resp) +} + +// UpdatePluginConfig update plugin config +// @Summary update plugin config +// @Description update plugin config +// @Tags AdminPlugin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdatePluginConfigReq true "UpdatePluginConfigReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/plugin/config [put] +func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) { + req := &schema.UpdatePluginConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + configFields, _ := json.Marshal(req.ConfigFields) + err := plugin.CallConfig(func(fn plugin.Config) error { + if fn.Info().SlugName == req.PluginSlugName { + return fn.ConfigReceiver(configFields) + } + return nil + }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + err = pc.pluginCommonService.UpdatePluginConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller_admin/role_controller.go b/internal/controller_admin/role_controller.go new file mode 100644 index 000000000..87484264c --- /dev/null +++ b/internal/controller_admin/role_controller.go @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + service "github.com/apache/answer/internal/service/role" + "github.com/gin-gonic/gin" +) + +// RoleController role controller +type RoleController struct { + roleService *service.RoleService +} + +// NewRoleController new controller +func NewRoleController(roleService *service.RoleService) *RoleController { + return &RoleController{roleService: roleService} +} + +// GetRoleList get role list +// @Summary get role list +// @Description get role list +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]schema.GetRoleResp} +// @Router /answer/admin/api/roles [get] +func (rc *RoleController) GetRoleList(ctx *gin.Context) { + req := &schema.GetRoleResp{} + if handler.BindAndCheck(ctx, req) { + return + } + resp, err := rc.roleService.GetRoleList(ctx) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller_admin/siteinfo_controller.go b/internal/controller_admin/siteinfo_controller.go new file mode 100644 index 000000000..8a92daba3 --- /dev/null +++ b/internal/controller_admin/siteinfo_controller.go @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "html" + "net/http" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/siteinfo" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" +) + +// SiteInfoController site info controller +type SiteInfoController struct { + siteInfoService *siteinfo.SiteInfoService +} + +// NewSiteInfoController new site info controller +func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { + return &SiteInfoController{ + siteInfoService: siteInfoService, + } +} + +// GetGeneral get site general information +// @Summary get site general information +// @Description get site general information +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} +// @Router /answer/admin/api/siteinfo/general [get] +func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteGeneral(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetInterface get site interface +// @Summary get site interface +// @Description get site interface +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} +// @Router /answer/admin/api/siteinfo/interface [get] +func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteInterface(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteBranding get site interface +// @Summary get site interface +// @Description get site interface +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp} +// @Router /answer/admin/api/siteinfo/branding [get] +func (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteBranding(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteWrite get site interface +// @Summary get site interface +// @Description get site interface +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteWriteResp} +// @Router /answer/admin/api/siteinfo/write [get] +func (sc *SiteInfoController) GetSiteWrite(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteWrite(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteLegal Set the legal information for the site +// @Summary Set the legal information for the site +// @Description Set the legal information for the site +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteLegalResp} +// @Router /answer/admin/api/siteinfo/legal [get] +func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteLegal(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSeo get site seo information +// @Summary get site seo information +// @Description get site seo information +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteSeoResp} +// @Router /answer/admin/api/siteinfo/seo [get] +func (sc *SiteInfoController) GetSeo(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSeo(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteLogin get site info login config +// @Summary get site info login config +// @Description get site info login config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteLoginResp} +// @Router /answer/admin/api/siteinfo/login [get] +func (sc *SiteInfoController) GetSiteLogin(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteLogin(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteCustomCssHTML get site info custom html css config +// @Summary get site info custom html css config +// @Description get site info custom html css config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteCustomCssHTMLResp} +// @Router /answer/admin/api/siteinfo/custom-css-html [get] +func (sc *SiteInfoController) GetSiteCustomCssHTML(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteTheme get site info theme config +// @Summary get site info theme config +// @Description get site info theme config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteThemeResp} +// @Router /answer/admin/api/siteinfo/theme [get] +func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteTheme(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetSiteUsers get site user config +// @Summary get site user config +// @Description get site user config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp} +// @Router /answer/admin/api/siteinfo/users [get] +func (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteUsers(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// GetRobots get site robots information +// @Summary get site robots information +// @Description get site robots information +// @Tags site +// @Produce json +// @Success 200 {string} txt "" +// @Router /robots.txt [get] +func (sc *SiteInfoController) GetRobots(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSeo(ctx) + if err != nil { + ctx.String(http.StatusOK, "") + return + } + ctx.String(http.StatusOK, resp.Robots) +} + +// GetCss get site custom CSS +// @Summary get site custom CSS +// @Description get site custom CSS +// @Tags site +// @Produce text/css +// @Success 200 {string} css "" +// @Router /custom.css [get] +func (sc *SiteInfoController) GetCss(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx) + if err != nil { + ctx.String(http.StatusOK, "") + return + } + ctx.Header("content-type", "text/css;charset=utf-8") + ctx.String(http.StatusOK, resp.CustomCss) +} + +// UpdateSeo update site seo information +// @Summary update site seo information +// @Description update site seo information +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteSeoReq true "seo" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/seo [put] +func (sc *SiteInfoController) UpdateSeo(ctx *gin.Context) { + req := schema.SiteSeoReq{} + if handler.BindAndCheck(ctx, &req) { + return + } + err := sc.siteInfoService.SaveSeo(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateGeneral update site general information +// @Summary update site general information +// @Description update site general information +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteGeneralReq true "general" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/general [put] +func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { + req := schema.SiteGeneralReq{} + if handler.BindAndCheck(ctx, &req) { + return + } + err := sc.siteInfoService.SaveSiteGeneral(ctx, req) + req.Name = html.UnescapeString(req.Name) + handler.HandleResponse(ctx, err, req) +} + +// UpdateInterface update site interface +// @Summary update site info interface +// @Description update site info interface +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteInterfaceReq true "general" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/interface [put] +func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) { + req := schema.SiteInterfaceReq{} + if handler.BindAndCheck(ctx, &req) { + return + } + err := sc.siteInfoService.SaveSiteInterface(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateBranding update site branding +// @Summary update site info branding +// @Description update site info branding +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteBrandingReq true "branding info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/branding [put] +func (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) { + req := &schema.SiteBrandingReq{} + if handler.BindAndCheck(ctx, req) { + return + } + currentBranding, getBrandingErr := sc.siteInfoService.GetSiteBranding(ctx) + if getBrandingErr == nil { + cleanUpErr := sc.siteInfoService.CleanUpRemovedBrandingFiles(ctx, req, currentBranding) + if cleanUpErr != nil { + log.Errorf("failed to clean up removed branding file(s): %v", cleanUpErr) + } + } else { + log.Errorf("failed to get current site branding: %v", getBrandingErr) + } + saveErr := sc.siteInfoService.SaveSiteBranding(ctx, req) + handler.HandleResponse(ctx, saveErr, nil) +} + +// UpdateSiteWrite update site write info +// @Summary update site write info +// @Description update site write info +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteWriteReq true "write info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/write [put] +func (sc *SiteInfoController) UpdateSiteWrite(ctx *gin.Context) { + req := &schema.SiteWriteReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, err := sc.siteInfoService.SaveSiteWrite(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateSiteLegal update site legal info +// @Summary update site legal info +// @Description update site legal info +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteLegalReq true "write info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/legal [put] +func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) { + req := &schema.SiteLegalReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.SaveSiteLegal(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateSiteLogin update site login +// @Summary update site login +// @Description update site login +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteLoginReq true "login info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/login [put] +func (sc *SiteInfoController) UpdateSiteLogin(ctx *gin.Context) { + req := &schema.SiteLoginReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.SaveSiteLogin(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateSiteCustomCssHTML update site custom css html config +// @Summary update site custom css html config +// @Description update site custom css html config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteCustomCssHTMLReq true "login info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/custom-css-html [put] +func (sc *SiteInfoController) UpdateSiteCustomCssHTML(ctx *gin.Context) { + req := &schema.SiteCustomCssHTMLReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.SaveSiteCustomCssHTML(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// SaveSiteTheme update site custom css html config +// @Summary update site custom css html config +// @Description update site custom css html config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteThemeReq true "login info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/theme [put] +func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) { + req := &schema.SiteThemeReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.SaveSiteTheme(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateSiteUsers update site config about users +// @Summary update site info config about users +// @Description update site info config about users +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SiteUsersReq true "users info" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/siteinfo/users [put] +func (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) { + req := &schema.SiteUsersReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.SaveSiteUsers(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// GetSMTPConfig get smtp config +// @Summary GetSMTPConfig get smtp config +// @Description GetSMTPConfig get smtp config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.GetSMTPConfigResp} +// @Router /answer/admin/api/setting/smtp [get] +func (sc *SiteInfoController) GetSMTPConfig(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetSMTPConfig(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateSMTPConfig update smtp config +// @Summary update smtp config +// @Description update smtp config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.UpdateSMTPConfigReq true "smtp config" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/setting/smtp [put] +func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) { + req := &schema.UpdateSMTPConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.UpdateSMTPConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// GetPrivilegesConfig get privileges config +// @Summary GetPrivilegesConfig get privileges config +// @Description GetPrivilegesConfig get privileges config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp} +// @Router /answer/admin/api/setting/privileges [get] +func (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) { + resp, err := sc.siteInfoService.GetPrivilegesConfig(ctx) + handler.HandleResponse(ctx, err, resp) +} + +// UpdatePrivilegesConfig update privileges config +// @Summary update privileges config +// @Description update privileges config +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.UpdatePrivilegesConfigReq true "config" +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/setting/privileges [put] +func (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) { + req := &schema.UpdatePrivilegesConfigReq{} + if handler.BindAndCheck(ctx, req) { + return + } + err := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller_admin/theme_controller.go b/internal/controller_admin/theme_controller.go new file mode 100644 index 000000000..d763f3d9e --- /dev/null +++ b/internal/controller_admin/theme_controller.go @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/gin-gonic/gin" +) + +type ThemeController struct{} + +// NewThemeController new theme controller. +func NewThemeController() *ThemeController { + return &ThemeController{} +} + +// GetThemeOptions godoc +// @Summary Get theme options +// @Description Get theme options +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Success 200 {object} handler.RespBody{} +// @Router /answer/admin/api/theme/options [get] +func (t *ThemeController) GetThemeOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, schema.GetThemeOptions) +} diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go new file mode 100644 index 000000000..00dfa2c3d --- /dev/null +++ b/internal/controller_admin/user_backyard_controller.go @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/user_admin" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" +) + +// UserAdminController user controller +type UserAdminController struct { + userService *user_admin.UserAdminService +} + +// NewUserAdminController new controller +func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdminController { + return &UserAdminController{userService: userService} +} + +// UpdateUserStatus update user +// @Summary update user +// @Description update user +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.UpdateUserStatusReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/user/status [put] +func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) { + if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + req := &schema.UpdateUserStatusReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + err := uc.userService.UpdateUserStatus(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// UpdateUserRole update user role +// @Summary update user role +// @Description update user role +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.UpdateUserRoleReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/user/role [put] +func (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) { + req := &schema.UpdateUserRoleReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + err := uc.userService.UpdateUserRole(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// AddUser add user +// @Summary add user +// @Description add user +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.AddUserReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/user [post] +func (uc *UserAdminController) AddUser(ctx *gin.Context) { + req := &schema.AddUserReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + err := uc.userService.AddUser(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// AddUsers add users +// @Summary add users +// @Description add users +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.AddUsersReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/users [post] +func (uc *UserAdminController) AddUsers(ctx *gin.Context) { + req := &schema.AddUsersReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userService.AddUsers(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// UpdateUserPassword update user password +// @Summary update user password +// @Description update user password +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.UpdateUserPasswordReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/user/password [put] +func (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) { + req := &schema.UpdateUserPasswordReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + err := uc.userService.UpdateUserPassword(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// EditUserProfile edit user profile +// @Summary edit user profile +// @Description edit user profile +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.EditUserProfileReq true "user" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/user/profile [put] +func (uc *UserAdminController) EditUserProfile(ctx *gin.Context) { + req := &schema.EditUserProfileReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.IsAdmin = middleware.GetUserIsAdminModerator(ctx) + if !req.IsAdmin { + handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil) + return + } + + errFields, err := uc.userService.EditUserProfile(ctx, req) + for _, field := range errFields { + field.ErrorMsg = translator.Tr(handler.GetLang(ctx), field.ErrorMsg) + } + handler.HandleResponse(ctx, err, errFields) +} + +// GetUserPage get user page +// @Summary get user page +// @Description get user page +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param page query int false "page size" +// @Param page_size query int false "page size" +// @Param query query string false "search query: email, username or id:[id]" +// @Param staff query bool false "staff user" +// @Param status query string false "user status" Enums(suspended, deleted, inactive) +// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}} +// @Router /answer/admin/api/users/page [get] +func (uc *UserAdminController) GetUserPage(ctx *gin.Context) { + req := &schema.GetUserPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userService.GetUserPage(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// GetUserActivation get user activation +// @Summary get user activation +// @Description get user activation +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param user_id query string true "user id" +// @Success 200 {object} handler.RespBody{data=schema.GetUserActivationResp} +// @Router /answer/admin/api/user/activation [get] +func (uc *UserAdminController) GetUserActivation(ctx *gin.Context) { + req := &schema.GetUserActivationReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := uc.userService.GetUserActivation(ctx, req) + handler.HandleResponse(ctx, err, resp) +} + +// SendUserActivation send user activation +// @Summary send user activation +// @Description send user activation +// @Security ApiKeyAuth +// @Tags admin +// @Produce json +// @Param data body schema.SendUserActivationReq true "SendUserActivationReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/users/activation [post] +func (uc *UserAdminController) SendUserActivation(ctx *gin.Context) { + req := &schema.SendUserActivationReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := uc.userService.SendUserActivation(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + +// DeletePermanently delete permanently +// @Summary delete permanently +// @Description delete permanently +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.DeletePermanentlyReq true "DeletePermanentlyReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/delete/permanently [delete] +func (uc *UserAdminController) DeletePermanently(ctx *gin.Context) { + req := &schema.DeletePermanentlyReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := uc.userService.DeletePermanently(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller_backyard/controller.go b/internal/controller_backyard/controller.go deleted file mode 100644 index 4c541f36f..000000000 --- a/internal/controller_backyard/controller.go +++ /dev/null @@ -1,11 +0,0 @@ -package controller_backyard - -import "github.com/google/wire" - -// ProviderSetController is controller providers. -var ProviderSetController = wire.NewSet( - NewReportController, - NewUserBackyardController, - NewThemeController, - NewSiteInfoController, -) diff --git a/internal/controller_backyard/report_controller.go b/internal/controller_backyard/report_controller.go deleted file mode 100644 index 3c20394b0..000000000 --- a/internal/controller_backyard/report_controller.go +++ /dev/null @@ -1,77 +0,0 @@ -package controller_backyard - -import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/report_backyard" - "github.com/answerdev/answer/pkg/converter" - "github.com/gin-gonic/gin" -) - -// ReportController report controller -type ReportController struct { - reportService *report_backyard.ReportBackyardService -} - -// NewReportController new controller -func NewReportController(reportService *report_backyard.ReportBackyardService) *ReportController { - return &ReportController{reportService: reportService} -} - -// ListReportPage godoc -// @Summary list report page -// @Description list report records -// @Security ApiKeyAuth -// @Tags admin -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param status query string true "status" Enums(pending, completed) -// @Param object_type query string true "object_type" Enums(all, question,answer,comment) -// @Param page query int false "page size" -// @Param page_size query int false "page size" -// @Success 200 {object} handler.RespBody -// @Router /answer/admin/api/reports/page [get] -func (rc *ReportController) ListReportPage(ctx *gin.Context) { - var ( - objectType = ctx.Query("object_type") - status = ctx.Query("status") - page = converter.StringToInt(ctx.DefaultQuery("page", "1")) - pageSize = converter.StringToInt(ctx.DefaultQuery("page_size", "20")) - ) - - dto := schema.GetReportListPageDTO{ - ObjectType: objectType, - Status: status, - Page: page, - PageSize: pageSize, - } - - resp, err := rc.reportService.ListReportPage(ctx, dto) - if err != nil { - handler.HandleResponse(ctx, err, schema.ErrTypeModal) - } else { - handler.HandleResponse(ctx, err, resp) - } -} - -// Handle godoc -// @Summary handle flag -// @Description handle flag -// @Security ApiKeyAuth -// @Tags admin -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param data body schema.ReportHandleReq true "flag" -// @Success 200 {object} handler.RespBody -// @Router /answer/admin/api/report/ [put] -func (rc *ReportController) Handle(ctx *gin.Context) { - req := schema.ReportHandleReq{} - if handler.BindAndCheck(ctx, &req) { - return - } - - err := rc.reportService.HandleReported(ctx, req) - handler.HandleResponse(ctx, err, nil) -} diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go deleted file mode 100644 index 30bfd1bfd..000000000 --- a/internal/controller_backyard/siteinfo_controller.go +++ /dev/null @@ -1,113 +0,0 @@ -package controller_backyard - -import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" - "github.com/gin-gonic/gin" -) - -type SiteInfoController struct { - siteInfoService *service.SiteInfoService -} - -// NewSiteInfoController new siteinfo controller. -func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { - return &SiteInfoController{ - siteInfoService: siteInfoService, - } -} - -// GetGeneral godoc -// @Summary Get siteinfo general -// @Description Get siteinfo general -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} -// @Router /answer/admin/api/siteinfo/general [get] -func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { - resp, err := sc.siteInfoService.GetSiteGeneral(ctx) - handler.HandleResponse(ctx, err, resp) -} - -// GetInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} -// @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" -func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { - resp, err := sc.siteInfoService.GetSiteInterface(ctx) - handler.HandleResponse(ctx, err, resp) -} - -// UpdateGeneral godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Param data body schema.SiteGeneralReq true "general" -// @Success 200 {object} handler.RespBody{} -// @Router /answer/admin/api/siteinfo/general [put] -func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { - req := schema.SiteGeneralReq{} - if handler.BindAndCheck(ctx, &req) { - return - } - err := sc.siteInfoService.SaveSiteGeneral(ctx, req) - handler.HandleResponse(ctx, err, nil) -} - -// UpdateInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Param data body schema.SiteInterfaceReq true "general" -// @Success 200 {object} handler.RespBody{} -// @Router /answer/admin/api/siteinfo/interface [put] -func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) { - req := schema.SiteInterfaceReq{} - if handler.BindAndCheck(ctx, &req) { - return - } - err := sc.siteInfoService.SaveSiteInterface(ctx, req) - handler.HandleResponse(ctx, err, nil) -} - -// GetSMTPConfig get smtp config -// @Summary GetSMTPConfig get smtp config -// @Description GetSMTPConfig get smtp config -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Success 200 {object} handler.RespBody{data=schema.GetSMTPConfigResp} -// @Router /answer/admin/api/setting/smtp [get] -func (sc *SiteInfoController) GetSMTPConfig(ctx *gin.Context) { - resp, err := sc.siteInfoService.GetSMTPConfig(ctx) - handler.HandleResponse(ctx, err, resp) -} - -// UpdateSMTPConfig update smtp config -// @Summary update smtp config -// @Description update smtp config -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Param data body schema.UpdateSMTPConfigReq true "smtp config" -// @Success 200 {object} handler.RespBody{} -// @Router /answer/admin/api/setting/smtp [put] -func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) { - req := &schema.UpdateSMTPConfigReq{} - if handler.BindAndCheck(ctx, req) { - return - } - err := sc.siteInfoService.UpdateSMTPConfig(ctx, req) - handler.HandleResponse(ctx, err, nil) -} diff --git a/internal/controller_backyard/theme_controller.go b/internal/controller_backyard/theme_controller.go deleted file mode 100644 index 69ce1d0a0..000000000 --- a/internal/controller_backyard/theme_controller.go +++ /dev/null @@ -1,26 +0,0 @@ -package controller_backyard - -import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/gin-gonic/gin" -) - -type ThemeController struct{} - -// NewThemeController new theme controller. -func NewThemeController() *ThemeController { - return &ThemeController{} -} - -// GetThemeOptions godoc -// @Summary Get theme options -// @Description Get theme options -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Success 200 {object} handler.RespBody{} -// @Router /answer/admin/api/theme/options [get] -func (t *ThemeController) GetThemeOptions(ctx *gin.Context) { - handler.HandleResponse(ctx, nil, schema.GetThemeOptions) -} diff --git a/internal/controller_backyard/user_backyard_controller.go b/internal/controller_backyard/user_backyard_controller.go deleted file mode 100644 index 0c3177fbd..000000000 --- a/internal/controller_backyard/user_backyard_controller.go +++ /dev/null @@ -1,61 +0,0 @@ -package controller_backyard - -import ( - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/user_backyard" - "github.com/gin-gonic/gin" -) - -// UserBackyardController user controller -type UserBackyardController struct { - userService *user_backyard.UserBackyardService -} - -// NewUserBackyardController new controller -func NewUserBackyardController(userService *user_backyard.UserBackyardService) *UserBackyardController { - return &UserBackyardController{userService: userService} -} - -// UpdateUserStatus update user -// @Summary update user -// @Description update user -// @Security ApiKeyAuth -// @Tags admin -// @Accept json -// @Produce json -// @Param data body schema.UpdateUserStatusReq true "user" -// @Success 200 {object} handler.RespBody -// @Router /answer/admin/api/user/status [put] -func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) { - req := &schema.UpdateUserStatusReq{} - if handler.BindAndCheck(ctx, req) { - return - } - - err := uc.userService.UpdateUserStatus(ctx, req) - handler.HandleResponse(ctx, err, nil) -} - -// GetUserPage get user page -// @Summary get user page -// @Description get user page -// @Security ApiKeyAuth -// @Tags admin -// @Produce json -// @Param page query int false "page size" -// @Param page_size query int false "page size" -// @Param username query string false "username" -// @Param e_mail query string false "email" -// @Param status query string false "user status" Enums(normal, suspended, deleted, inactive) -// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}} -// @Router /answer/admin/api/users/page [get] -func (uc *UserBackyardController) GetUserPage(ctx *gin.Context) { - req := &schema.GetUserPageReq{} - if handler.BindAndCheck(ctx, req) { - return - } - - resp, err := uc.userService.GetUserPage(ctx, req) - handler.HandleResponse(ctx, err, resp) -} diff --git a/internal/entity/activity_entity.go b/internal/entity/activity_entity.go index 3e7a7c165..4f0155e47 100644 --- a/internal/entity/activity_entity.go +++ b/internal/entity/activity_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -9,22 +28,35 @@ const ( // Activity activity type Activity struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` - UserID string `xorm:"not null index BIGINT(20) user_id"` - TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` - ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` - ActivityType int `xorm:"not null INT(11) activity_type"` - Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"` - Rank int `xorm:"not null default 0 INT(11) rank"` - HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` + UserID string `xorm:"not null index BIGINT(20) user_id"` + TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` + ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` + OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` + ActivityType int `xorm:"not null INT(11) activity_type"` + Cancelled int `xorm:"not null default 0 TINYINT(4) cancelled"` + Rank int `xorm:"not null default 0 INT(11) rank"` + HasRank int `xorm:"not null default 0 TINYINT(4) has_rank"` + RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` } type ActivityRankSum struct { Rank int `xorm:"not null default 0 INT(11) rank"` } +type ActivityUserRankStat struct { + UserID string `xorm:"user_id"` + Rank int `xorm:"rank_amount"` +} + +type ActivityUserVoteStat struct { + UserID string `xorm:"user_id"` + VoteCount int `xorm:"vote_count"` +} + // TableName activity table name func (Activity) TableName() string { return "activity" diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 3666f1065..4c9436ecb 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -1,54 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" const ( - Answer_Search_OrderBy_Default = "default" - Answer_Search_OrderBy_Time = "updated" - Answer_Search_OrderBy_Vote = "vote" + AnswerSearchOrderByDefault = "default" + AnswerSearchOrderByTime = "updated" + AnswerSearchOrderByVote = "vote" + AnswerSearchOrderByTimeAsc = "created" AnswerStatusAvailable = 1 AnswerStatusDeleted = 10 + AnswerStatusPending = 11 ) -var CmsAnswerSearchStatus = map[string]int{ +var AdminAnswerSearchStatus = map[string]int{ "available": AnswerStatusAvailable, "deleted": AnswerStatusDeleted, + "pending": AnswerStatusPending, } // Answer answer type Answer struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` - UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` - OriginalText string `xorm:"not null MEDIUMTEXT original_text"` - ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` - Status int `xorm:"not null default 1 INT(11) status"` - Adopted int `xorm:"not null default 1 INT(11) adopted"` - CommentCount int `xorm:"not null default 0 INT(11) comment_count"` - VoteCount int `xorm:"not null default 0 INT(11) vote_count"` - RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Accepted int `xorm:"not null default 1 INT(11) adopted"` + CommentCount int `xorm:"not null default 0 INT(11) comment_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` } type AnswerSearch struct { Answer - Order string `json:"order_by" ` // default or updated - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size -} - -type CmsAnswerSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 Deleted + IncludeDeleted bool `json:"include_deleted"` + LoginUserID string `json:"login_user_id"` + Order string `json:"order_by"` // default or updated + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size } -type AdminSetAnswerStatusRequest struct { - StatusStr string `json:"status" form:"status"` - AnswerID string `json:"answer_id" form:"answer_id"` +type PersonalAnswerPageQueryCond struct { + Page int + PageSize int + UserID string + Order string + ShowPending bool } // TableName answer table name diff --git a/internal/entity/auth_user_entity.go b/internal/entity/auth_user_entity.go index d1f366a43..29a639d3a 100644 --- a/internal/entity/auth_user_entity.go +++ b/internal/entity/auth_user_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity // UserCacheInfo User Cache Information @@ -5,4 +24,7 @@ type UserCacheInfo struct { UserID string `json:"user_id"` UserStatus int `json:"user_status"` EmailStatus int `json:"email_status"` + RoleID int `json:"role_id"` + ExternalID string `json:"external_id"` + VisitToken string `json:"visit_token"` } diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go new file mode 100644 index 000000000..339b9ca06 --- /dev/null +++ b/internal/entity/badge_award_entity.go @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +const ( + IsBadgeNotDeleted = 0 + IsBadgeDeleted = 1 + + BadgeEmptyAwardKey = "0" +) + +// BadgeAward badge_award +type BadgeAward struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + UserID string `xorm:"not null index BIGINT(20) user_id"` + BadgeID string `xorm:"not null index BIGINT(20) badge_id"` + AwardKey string `xorm:"not null index VARCHAR(64) award_key"` + BadgeGroupID int64 `xorm:"not null index BIGINT(20) badge_group_id"` + IsBadgeDeleted int8 `xorm:"not null TINYINT(1) is_badge_deleted"` +} + +// TableName badge_award table name +func (BadgeAward) TableName() string { + return "badge_award" +} + +type BadgeEarnedCount struct { + BadgeID string `xorm:"badge_id"` + EarnedCount int64 `xorm:"earned_count"` +} + +// TableName badge_award table name +func (BadgeEarnedCount) TableName() string { + return "badge_award" +} + +type BadgeAwardRecent struct { + Created time.Time `xorm:"created"` + BadgeID string `xorm:"badge_id"` + AwardKey string `xorm:"award_key"` + EarnedCount int64 `xorm:"earned_count"` + IsBadgeDeleted int8 `xorm:"is_badge_deleted"` +} + +// TableName badge_award table name +func (BadgeAwardRecent) TableName() string { + return "badge_award" +} diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go new file mode 100644 index 000000000..a370e2750 --- /dev/null +++ b/internal/entity/badge_entity.go @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import ( + "github.com/tidwall/gjson" + "time" +) + +type BadgeLevel int + +const ( + BadgeStatusActive = 1 + BadgeStatusDeleted = 10 + BadgeStatusInactive = 11 + + BadgeLevelBronze BadgeLevel = 1 + BadgeLevelSilver BadgeLevel = 2 + BadgeLevelGold BadgeLevel = 3 + + BadgeSingleAward = 1 + BadgeMultiAward = 2 +) + +// Badge badge +type Badge struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + Name string `xorm:"not null default '' VARCHAR(256) name"` + Icon string `xorm:"not null default '' VARCHAR(1024) icon"` + AwardCount int `xorm:"not null default 0 INT(11) award_count"` + Description string `xorm:"not null MEDIUMTEXT description"` + Status int8 `xorm:"not null default 1 INT(11) status"` + BadgeGroupID int64 `xorm:"not null default 0 BIGINT(20) badge_group_id"` + Level BadgeLevel `xorm:"not null default 1 TINYINT(4) level"` + Single int8 `xorm:"not null default 1 TINYINT(4) single"` + Collect string `xorm:"not null default '' VARCHAR(128) collect"` + Handler string `xorm:"not null default '' VARCHAR(128) handler"` + Param string `xorm:"not null TEXT param"` +} + +// TableName badge table name +func (b *Badge) TableName() string { + return "badge" +} + +func (b *Badge) GetIntParam(key string) int64 { + return gjson.Get(b.Param, key).Int() +} + +func (b *Badge) GetStringParam(key string) string { + return gjson.Get(b.Param, key).String() +} diff --git a/internal/entity/badge_group_entity.go b/internal/entity/badge_group_entity.go new file mode 100644 index 000000000..3be4d8209 --- /dev/null +++ b/internal/entity/badge_group_entity.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// BadgeGroup badge_group +type BadgeGroup struct { + ID string `json:"id" xorm:"not null pk autoincr BIGINT(20) id"` + Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` + CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` +} + +// TableName badge_group table name +func (BadgeGroup) TableName() string { + return "badge_group" +} diff --git a/internal/entity/captcha_entity.go b/internal/entity/captcha_entity.go new file mode 100644 index 000000000..28d5ad46b --- /dev/null +++ b/internal/entity/captcha_entity.go @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +const ( + CaptchaActionEmail = "email" + CaptchaActionPassword = "password" + CaptchaActionEditUserinfo = "edit_userinfo" + CaptchaActionQuestion = "question" + CaptchaActionAnswer = "answer" + CaptchaActionComment = "comment" + CaptchaActionEdit = "edit" + CaptchaActionInvitationAnswer = "invitation_answer" + CaptchaActionSearch = "search" + CaptchaActionReport = "report" + CaptchaActionDelete = "delete" + CaptchaActionVote = "vote" +) + +type ActionRecordInfo struct { + LastTime int64 `json:"last_time"` + Num int `json:"num"` + Config string `json:"config"` +} diff --git a/internal/entity/collection_entity.go b/internal/entity/collection_entity.go index c37c12344..5fbd5b0eb 100644 --- a/internal/entity/collection_entity.go +++ b/internal/entity/collection_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" diff --git a/internal/entity/collection_group_entity.go b/internal/entity/collection_group_entity.go index cbb860db3..7ae1f5beb 100644 --- a/internal/entity/collection_group_entity.go +++ b/internal/entity/collection_group_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" diff --git a/internal/entity/comment_entity.go b/internal/entity/comment_entity.go index bf8ad46db..c96f6b3c1 100644 --- a/internal/entity/comment_entity.go +++ b/internal/entity/comment_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import ( @@ -5,12 +24,13 @@ import ( "fmt" "time" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/pkg/converter" ) const ( CommentStatusAvailable = 1 CommentStatusDeleted = 10 + CommentStatusPending = 11 ) // Comment comment diff --git a/internal/entity/config_entity.go b/internal/entity/config_entity.go index 1d716a2d3..95a02be48 100644 --- a/internal/entity/config_entity.go +++ b/internal/entity/config_entity.go @@ -1,13 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity +import ( + "encoding/json" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/pkg/converter" +) + // Config config type Config struct { ID int `xorm:"not null pk autoincr INT(11) id"` - Key string `xorm:"unique VARCHAR(32) key"` + Key string `xorm:"unique VARCHAR(128) key"` Value string `xorm:"TEXT value"` } // TableName config table name -func (Config) TableName() string { +func (c *Config) TableName() string { return "config" } + +func (c *Config) BuildByJSON(data []byte) { + cf := &Config{} + _ = json.Unmarshal(data, cf) + c.ID = cf.ID + c.Key = cf.Key + c.Value = cf.Value +} + +func (c *Config) JsonString() string { + data, _ := json.Marshal(c) + return string(data) +} + +// GetIntValue get int value +func (c *Config) GetIntValue() int { + if len(c.Value) == 0 { + log.Warnf("config value is empty, key: %s, value: %s", c.Key, c.Value) + } + return converter.StringToInt(c.Value) +} + +// GetArrayStringValue get array string value +func (c *Config) GetArrayStringValue() []string { + var arr []string + _ = json.Unmarshal([]byte(c.Value), &arr) + return arr +} + +// GetByteValue get byte value +func (c *Config) GetByteValue() []byte { + return []byte(c.Value) +} diff --git a/internal/entity/file_record_entity.go b/internal/entity/file_record_entity.go new file mode 100644 index 000000000..83a917aac --- /dev/null +++ b/internal/entity/file_record_entity.go @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +const ( + FileRecordStatusAvailable = 1 + FileRecordStatusDeleted = 10 +) + +// FileRecord file record +type FileRecord struct { + ID int `xorm:"not null pk autoincr INT(10) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + FilePath string `xorm:"not null VARCHAR(256) file_path"` + FileURL string `xorm:"not null VARCHAR(1024) file_url"` + ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` + Source string `xorm:"not null VARCHAR(128) source"` + Status int `xorm:"not null default 0 TINYINT(4) status"` +} + +// TableName file record table name +func (FileRecord) TableName() string { + return "file_record" +} diff --git a/internal/entity/meta_entity.go b/internal/entity/meta_entity.go index 17ff38323..203e83c56 100644 --- a/internal/entity/meta_entity.go +++ b/internal/entity/meta_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -7,6 +26,7 @@ const ( QuestionCloseReasonKey = "question.close.reason" AnswerEditSummaryKey = "answer.edit.summary" TagEditSummaryKey = "tag.edit.summary" + ObjectReactSummaryKey = "object.react.summary" ) // Meta meta diff --git a/internal/entity/notification_entity.go b/internal/entity/notification_entity.go index d16fa06aa..366719682 100644 --- a/internal/entity/notification_entity.go +++ b/internal/entity/notification_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -11,6 +30,7 @@ type Notification struct { ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` Content string `xorm:"not null TEXT content"` Type int `xorm:"not null default 0 INT(11) type"` + MsgType int `xorm:"not null default 0 INT(11) msg_type"` IsRead int `xorm:"not null default 1 INT(11) is_read"` Status int `xorm:"not null default 1 INT(11) status"` } diff --git a/internal/entity/plugin_config_entity.go b/internal/entity/plugin_config_entity.go new file mode 100644 index 000000000..6233d263a --- /dev/null +++ b/internal/entity/plugin_config_entity.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +// PluginConfig plugin config +type PluginConfig struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + PluginSlugName string `xorm:"unique VARCHAR(128) plugin_slug_name"` + Value string `xorm:"TEXT value"` +} + +// TableName config table name +func (PluginConfig) TableName() string { + return "plugin_config" +} diff --git a/internal/entity/plugin_kv_storage_entity.go b/internal/entity/plugin_kv_storage_entity.go new file mode 100644 index 000000000..c7e6efbe3 --- /dev/null +++ b/internal/entity/plugin_kv_storage_entity.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +type PluginKVStorage struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + PluginSlugName string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) plugin_slug_name"` + Group string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) 'group'"` + Key string `xorm:"not null VARCHAR(128) UNIQUE(uk_psg) 'key'"` + Value string `xorm:"not null TEXT value"` +} + +func (PluginKVStorage) TableName() string { + return "plugin_kv_storage" +} diff --git a/internal/entity/plugin_user_config_entity.go b/internal/entity/plugin_user_config_entity.go new file mode 100644 index 000000000..edf93c9f3 --- /dev/null +++ b/internal/entity/plugin_user_config_entity.go @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +// PluginUserConfig plugin config +type PluginUserConfig struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + UserID string `xorm:"not null default 0 BIGINT(20) UNIQUE(uk_up) user_id"` + PluginSlugName string `xorm:"VARCHAR(128) UNIQUE(uk_up) plugin_slug_name"` + Value string `xorm:"TEXT value"` +} + +// TableName config table name +func (PluginUserConfig) TableName() string { + return "plugin_user_config" +} diff --git a/internal/entity/power_entity.go b/internal/entity/power_entity.go new file mode 100644 index 000000000..e974fd6db --- /dev/null +++ b/internal/entity/power_entity.go @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// Power power +type Power struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + Name string `xorm:"not null default '' VARCHAR(50) name"` + PowerType string `xorm:"not null default '' VARCHAR(100) power_type"` + Description string `xorm:"not null default '' VARCHAR(200) description"` +} + +// TableName power table name +func (Power) TableName() string { + return "power" +} diff --git a/internal/entity/question_entity.go b/internal/entity/question_entity.go index 83de9520a..9e5dcd112 100644 --- a/internal/entity/question_entity.go +++ b/internal/entity/question_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import ( @@ -6,50 +25,76 @@ import ( const ( QuestionStatusAvailable = 1 - QuestionStatusclosed = 2 + QuestionStatusClosed = 2 QuestionStatusDeleted = 10 + QuestionStatusPending = 11 + QuestionUnPin = 1 + QuestionPin = 2 + QuestionShow = 1 + QuestionHide = 2 ) -var CmsQuestionSearchStatus = map[string]int{ +var AdminQuestionSearchStatus = map[string]int{ "available": QuestionStatusAvailable, - "closed": QuestionStatusclosed, + "closed": QuestionStatusClosed, "deleted": QuestionStatusDeleted, + "pending": QuestionStatusPending, } -var CmsQuestionSearchStatusIntToString = map[int]string{ +var AdminQuestionSearchStatusIntToString = map[int]string{ QuestionStatusAvailable: "available", - QuestionStatusclosed: "closed", + QuestionStatusClosed: "closed", QuestionStatusDeleted: "deleted", -} - -type QuestionTag struct { - Question `xorm:"extends"` - TagRel `xorm:"extends"` + QuestionStatusPending: "pending", } // Question question type Question struct { ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + InviteUserID string `xorm:"TEXT invite_user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` Title string `xorm:"not null default '' VARCHAR(150) title"` OriginalText string `xorm:"not null MEDIUMTEXT original_text"` ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` Status int `xorm:"not null default 1 INT(11) status"` ViewCount int `xorm:"not null default 0 INT(11) view_count"` UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` VoteCount int `xorm:"not null default 0 INT(11) vote_count"` AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + HotScore int `xorm:"not null default 0 INT(11) hot_score"` CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` FollowCount int `xorm:"not null default 0 INT(11) follow_count"` AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` - PostUpdateTime time.Time `xorm:"default CURRENT_TIMESTAMP TIMESTAMP post_update_time"` - RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + LinkedCount int `xorm:"not null default 0 INT(11) linked_count"` } // TableName question table name func (Question) TableName() string { return "question" } + +// QuestionWithTagsRevision question +type QuestionWithTagsRevision struct { + Question + Tags []*TagSimpleInfoForRevision `json:"tags"` +} + +// TagSimpleInfoForRevision tag simple info for revision +type TagSimpleInfoForRevision struct { + ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` + MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"` + MainTagSlugName string `xorm:"not null default '' VARCHAR(35) main_tag_slug_name"` + SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` + DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"` + Recommend bool `xorm:"not null default false BOOL recommend"` + Reserved bool `xorm:"not null default false BOOL reserved"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` +} diff --git a/internal/entity/question_link_entity.go b/internal/entity/question_link_entity.go new file mode 100644 index 000000000..825e7595b --- /dev/null +++ b/internal/entity/question_link_entity.go @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import ( + "time" +) + +const ( + QuestionLinkStatusAvailable = 1 + QuestionLinkStatusDeleted = 2 +) + +type QuestionLink struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + FromQuestionID string `xorm:"not null default 0 BIGINT(20) index from_question_id"` + FromAnswerID string `xorm:"BIGINT(20) from_answer_id"` + ToQuestionID string `xorm:"not null default 0 BIGINT(20) index to_question_id"` + ToAnswerID string `xorm:"BIGINT(20) to_answer_id"` + Status int `xorm:"not null default 1 INT(11) status"` +} + +func (QuestionLink) TableName() string { + return "question_link" +} diff --git a/internal/entity/report_entity.go b/internal/entity/report_entity.go index 55820d260..07f5bc840 100644 --- a/internal/entity/report_entity.go +++ b/internal/entity/report_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -5,6 +24,7 @@ import "time" const ( ReportStatusPending = 1 ReportStatusCompleted = 2 + ReportStatusIgnore = 3 ReportStatusDeleted = 10 ) diff --git a/internal/entity/review_entity.go b/internal/entity/review_entity.go new file mode 100644 index 000000000..c88de0074 --- /dev/null +++ b/internal/entity/review_entity.go @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +const ( + ReviewStatusPending = 1 + ReviewStatusApproved = 2 + ReviewStatusRejected = 3 +) + +// Review review +type Review struct { + ID int `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null BIGINT(20) user_id"` + ObjectID string `xorm:"not null BIGINT(20) object_id"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ReviewerUserID string `xorm:"not null default 0 BIGINT(20) reviewer_user_id"` + Submitter string `xorm:"not null default '' VARCHAR(100) submitter"` + Reason string `xorm:"not null TEXT reason"` + Status int `xorm:"not null default 0 INT(11) status"` +} + +// TableName review table name +func (Review) TableName() string { + return "review" +} diff --git a/internal/entity/revision_entity.go b/internal/entity/revision_entity.go index daf59a370..7a5eb5b2e 100644 --- a/internal/entity/revision_entity.go +++ b/internal/entity/revision_entity.go @@ -1,19 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity -import "time" +import ( + "time" +) + +const ( + // RevisionNormalStatus this revision is normal + RevisionNormalStatus = 0 + // RevisionUnreviewedStatus this revision is unreviewed + RevisionUnreviewedStatus = 1 + // RevisionReviewPassStatus this revision is reviewed and approved by operator + RevisionReviewPassStatus = 2 + // RevisionReviewRejectStatus this revision is reviewed and rejected by operator + RevisionReviewRejectStatus = 3 +) // Revision revision type Revision struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` - CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` - UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` - UserID string `xorm:"not null default 0 BIGINT(20) user_id"` - ObjectType int `xorm:"not null default 0 ) INT(11) object_type"` - ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` - Title string `xorm:"not null default '' VARCHAR(255) title"` - Content string `xorm:"not null TEXT content"` - Log string `xorm:"VARCHAR(255) log"` - Status int `xorm:"not null default 1 INT(11) status"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` + Title string `xorm:"not null default '' VARCHAR(255) title"` + Content string `xorm:"not null MEDIUMTEXT content"` + Log string `xorm:"VARCHAR(255) log"` + Status int `xorm:"not null default 1 INT(11) status"` + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` } // TableName revision table name diff --git a/internal/entity/role_entity.go b/internal/entity/role_entity.go new file mode 100644 index 000000000..76afe16d5 --- /dev/null +++ b/internal/entity/role_entity.go @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// Role role +type Role struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + Name string `xorm:"not null default '' VARCHAR(50) name"` + Description string `xorm:"not null default '' VARCHAR(200) description"` +} + +// TableName user table name +func (Role) TableName() string { + return "role" +} diff --git a/internal/entity/role_power_rel_entity.go b/internal/entity/role_power_rel_entity.go new file mode 100644 index 000000000..7c4247169 --- /dev/null +++ b/internal/entity/role_power_rel_entity.go @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// RolePowerRel role power rel +type RolePowerRel struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + RoleID int `xorm:"not null default 0 INT(11) role_id"` + PowerType string `xorm:"not null default '' VARCHAR(200) power_type"` +} + +// TableName role power rel table name +func (RolePowerRel) TableName() string { + return "role_power_rel" +} diff --git a/internal/entity/site_info.go b/internal/entity/site_info.go index 0d8d29b01..7a013c2a3 100644 --- a/internal/entity/site_info.go +++ b/internal/entity/site_info.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" diff --git a/internal/entity/tag_entity.go b/internal/entity/tag_entity.go index 2bef34d03..7c405139f 100644 --- a/internal/entity/tag_entity.go +++ b/internal/entity/tag_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -7,6 +26,11 @@ const ( TagStatusDeleted = 10 ) +var TagStatusDisplayMapping = map[int]string{ + TagStatusAvailable: "available", + TagStatusDeleted: "deleted", +} + // Tag tag type Tag struct { ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` @@ -21,7 +45,10 @@ type Tag struct { FollowCount int `xorm:"not null default 0 INT(11) follow_count"` QuestionCount int `xorm:"not null default 0 INT(11) question_count"` Status int `xorm:"not null default 1 INT(11) status"` + Recommend bool `xorm:"not null default false BOOL recommend"` + Reserved bool `xorm:"not null default false BOOL reserved"` RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` } // TableName tag table name diff --git a/internal/entity/tag_rel_entity.go b/internal/entity/tag_rel_entity.go index 859f8bf75..067996cfd 100644 --- a/internal/entity/tag_rel_entity.go +++ b/internal/entity/tag_rel_entity.go @@ -1,9 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" const ( TagRelStatusAvailable = 1 + TagRelStatusHide = 2 TagRelStatusDeleted = 10 ) diff --git a/internal/entity/uniqid_entity.go b/internal/entity/uniqid_entity.go index dd0e2e7b5..a9d7b353e 100644 --- a/internal/entity/uniqid_entity.go +++ b/internal/entity/uniqid_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity // Uniqid uniqid diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index f87e651ed..66d612926 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity import "time" @@ -17,12 +36,16 @@ const ( UserAdminFlag = 1 ) +// PermanentSuspensionTime is a fixed time representing permanent suspension (2099-12-31 23:59:59) +var PermanentSuspensionTime = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) + // User user type User struct { ID string `xorm:"not null pk autoincr BIGINT(20) id"` CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` SuspendedAt time.Time `xorm:"TIMESTAMP suspended_at"` + SuspendedUntil time.Time `xorm:"DATETIME suspended_until"` DeletedAt time.Time `xorm:"TIMESTAMP deleted_at"` LastLoginDate time.Time `xorm:"TIMESTAMP last_login_date"` Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"` @@ -37,14 +60,16 @@ type User struct { Status int `xorm:"not null default 1 INT(11) status"` AuthorityGroup int `xorm:"not null default 1 INT(11) authority_group"` DisplayName string `xorm:"not null default '' VARCHAR(30) display_name"` - Avatar string `xorm:"not null default '' VARCHAR(255) avatar"` + Avatar string `xorm:"not null default '' VARCHAR(1024) avatar"` Mobile string `xorm:"not null VARCHAR(20) mobile"` Bio string `xorm:"not null TEXT bio"` - BioHtml string `xorm:"not null TEXT bio_html"` + BioHTML string `xorm:"not null TEXT bio_html"` Website string `xorm:"not null default '' VARCHAR(255) website"` Location string `xorm:"not null default '' VARCHAR(100) location"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` IsAdmin bool `xorm:"not null default false BOOL is_admin"` + Language string `xorm:"not null default '' VARCHAR(100) language"` + ColorScheme string `xorm:"not null default '' VARCHAR(100) color_scheme"` } // TableName user table name @@ -54,6 +79,6 @@ func (User) TableName() string { type UserSearch struct { User - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size } diff --git a/internal/entity/user_external_login_entity.go b/internal/entity/user_external_login_entity.go new file mode 100644 index 000000000..81d5c7b08 --- /dev/null +++ b/internal/entity/user_external_login_entity.go @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// UserExternalLogin user external login +type UserExternalLogin struct { + ID int64 `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + Provider string `xorm:"not null default '' VARCHAR(100) provider"` + ExternalID string `xorm:"not null default '' VARCHAR(128) external_id"` + MetaInfo string `xorm:"TEXT meta_info"` +} + +// TableName table name +func (UserExternalLogin) TableName() string { + return "user_external_login" +} diff --git a/internal/entity/user_notification_config_entity.go b/internal/entity/user_notification_config_entity.go new file mode 100644 index 000000000..94a1e76d1 --- /dev/null +++ b/internal/entity/user_notification_config_entity.go @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// UserNotificationConfig user notification config +type UserNotificationConfig struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 INDEX UNIQUE(uk_us) BIGINT(20) INDEX user_id"` + Source string `xorm:"not null default '' INDEX UNIQUE(uk_us) VARCHAR(64) source"` + Channels string `xorm:"not null TEXT channels"` + Enabled bool `xorm:"not null default false BOOL enabled"` +} + +// TableName notification table name +func (UserNotificationConfig) TableName() string { + return "user_notification_config" +} diff --git a/internal/entity/user_role_rel_entity.go b/internal/entity/user_role_rel_entity.go new file mode 100644 index 000000000..4ef74f8ed --- /dev/null +++ b/internal/entity/user_role_rel_entity.go @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// UserRoleRel role +type UserRoleRel struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + RoleID int `xorm:"not null default 0 INT(11) role_id"` +} + +// TableName user role rel table name +func (UserRoleRel) TableName() string { + return "user_role_rel" +} diff --git a/internal/entity/version_entity.go b/internal/entity/version_entity.go index 21a5d87c8..7213f9907 100644 --- a/internal/entity/version_entity.go +++ b/internal/entity/version_entity.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package entity // Version version diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go new file mode 100644 index 000000000..d43bbb45f --- /dev/null +++ b/internal/install/install_controller.go @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package install + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/apache/answer/configs" + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/cli" + "github.com/apache/answer/internal/migrations" + "github.com/apache/answer/internal/schema" + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +// LangOptions get installation language options +// @Summary get installation language options +// @Description get installation language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]translator.LangOption} +// @Router /installation/language/options [get] +func LangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// GetLangMapping get installation language config mapping +// @Summary get installation language config mapping +// @Description get installation language config mapping +// @Tags Lang +// @Param lang query string true "installation language" +// @Produce json +// @Success 200 {object} handler.RespBody{} +// @Router /installation/language/config [get] +func GetLangMapping(ctx *gin.Context) { + t, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath}) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + lang := ctx.Query("lang") + trData, _ := t.Dump(i18n.Language(lang)) + var resp map[string]any + _ = json.Unmarshal(trData, &resp) + handler.HandleResponse(ctx, nil, resp) +} + +// CheckConfigFileAndRedirectToInstallPage if config file not exist try to redirect to install page +// @Summary if config file not exist try to redirect to install page +// @Description if config file not exist try to redirect to install page +// @Tags installation +// @Accept json +// @Produce json +// @Router / [get] +func CheckConfigFileAndRedirectToInstallPage(ctx *gin.Context) { + if cli.CheckConfigFile(confPath) { + ctx.Redirect(http.StatusFound, "/50x") + } else { + ctx.Redirect(http.StatusFound, "/install") + } +} + +// CheckConfigFile check config file if exist when installation +// @Summary check config file if exist when installation +// @Description check config file if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/config-file/check [post] +func CheckConfigFile(ctx *gin.Context) { + resp := &CheckConfigFileResp{} + resp.ConfigFileExist = cli.CheckConfigFile(confPath) + if !resp.ConfigFileExist { + handler.HandleResponse(ctx, nil, resp) + return + } + allConfig, err := conf.ReadConfig(confPath) + if err != nil { + log.Error(err) + err = errors.BadRequest(reason.ReadConfigFailed) + handler.HandleResponse(ctx, err, nil) + return + } + resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database) + if resp.DBConnectionSuccess { + resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database) + } + handler.HandleResponse(ctx, nil, resp) +} + +// CheckDatabase check database if exist when installation +// @Summary check database if exist when installation +// @Description check database if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq" +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/db/check [post] +func CheckDatabase(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp := &CheckDatabaseResp{} + dataConf := &data.Database{ + Driver: req.DbType, + Connection: req.GetConnection(), + } + resp.ConnectionSuccess = cli.CheckDBConnection(dataConf) + if !resp.ConnectionSuccess { + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + handler.HandleResponse(ctx, nil, resp) +} + +// InitEnvironment init environment +// @Summary init environment +// @Description init environment +// @Tags installation +// @Accept json +// @Produce json +// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq" +// @Success 200 {object} handler.RespBody{} +// @Router /installation/init [post] +func InitEnvironment(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + // check config file if exist + if cli.CheckConfigFile(confPath) { + log.Debug("config file already exists") + handler.HandleResponse(ctx, nil, nil) + return + } + + if err := cli.InstallConfigFile(confPath); err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ + Success: false, + CreateConfigFailed: true, + DefaultConfig: string(configs.Config), + ErrType: schema.ErrTypeAlert.ErrType, + }) + return + } + + c, err := conf.ReadConfig(confPath) + if err != nil { + log.Errorf("read config failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) + return + } + c.Data.Database.Driver = req.DbType + c.Data.Database.Connection = req.GetConnection() + c.Data.Cache.FilePath = filepath.Join(cli.CacheDir, cli.DefaultCacheFileName) + c.I18n.BundleDir = cli.I18nPath + c.ServiceConfig.UploadPath = cli.UploadFilePath + + if err := conf.RewriteConfig(confPath, c); err != nil { + log.Errorf("rewrite config failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) + return + } + handler.HandleResponse(ctx, nil, nil) +} + +// InitBaseInfo init base info +// @Summary init base info +// @Description init base info +// @Tags installation +// @Accept json +// @Produce json +// @Param data body install.InitBaseInfoReq true "InitBaseInfoReq" +// @Success 200 {object} handler.RespBody{} +// @Router /installation/base-info [post] +func InitBaseInfo(ctx *gin.Context) { + req := &InitBaseInfoReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.FormatSiteUrl() + + c, err := conf.ReadConfig(confPath) + if err != nil { + log.Errorf("read config failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) + return + } + + if cli.CheckDBTableExist(c.Data.Database) { + log.Warn("database is already initialized") + handler.HandleResponse(ctx, nil, nil) + return + } + + engine, err := data.NewDB(false, c.Data.Database) + if err != nil { + log.Errorf("init database failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil) + } + + inputData := &migrations.InitNeedUserInputData{} + _ = copier.Copy(inputData, req) + if err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert) + return + } + + handler.HandleResponse(ctx, nil, nil) + go func() { + time.Sleep(1 * time.Second) + os.Exit(0) + }() +} diff --git a/internal/install/install_from_env.go b/internal/install/install_from_env.go new file mode 100644 index 000000000..a6b668bab --- /dev/null +++ b/internal/install/install_from_env.go @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package install + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + + "github.com/gin-gonic/gin" + "github.com/tidwall/gjson" +) + +type Env struct { + AutoInstall string `json:"auto_install"` + DbType string `json:"db_type"` + DbUsername string `json:"db_username"` + DbPassword string `json:"db_password"` + DbHost string `json:"db_host"` + DbName string `json:"db_name"` + DbFile string `json:"db_file"` + Language string `json:"lang"` + + SiteName string `json:"site_name"` + SiteURL string `json:"site_url"` + ContactEmail string `json:"contact_email"` + AdminName string `json:"name"` + AdminPassword string `json:"password"` + AdminEmail string `json:"email"` + LoginRequired bool `json:"login_required"` + ExternalContentDisplay string `json:"external_content_display"` +} + +func TryToInstallByEnv() (installByEnv bool, err error) { + env := loadEnv() + if len(env.AutoInstall) == 0 { + return false, nil + } + fmt.Println("[auto-install] try to install by environment variable") + return true, initByEnv(env) +} + +func loadEnv() (env *Env) { + return &Env{ + AutoInstall: os.Getenv("AUTO_INSTALL"), + DbType: os.Getenv("DB_TYPE"), + DbUsername: os.Getenv("DB_USERNAME"), + DbPassword: os.Getenv("DB_PASSWORD"), + DbHost: os.Getenv("DB_HOST"), + DbName: os.Getenv("DB_NAME"), + DbFile: os.Getenv("DB_FILE"), + Language: os.Getenv("LANGUAGE"), + SiteName: os.Getenv("SITE_NAME"), + SiteURL: os.Getenv("SITE_URL"), + ContactEmail: os.Getenv("CONTACT_EMAIL"), + AdminName: os.Getenv("ADMIN_NAME"), + AdminPassword: os.Getenv("ADMIN_PASSWORD"), + AdminEmail: os.Getenv("ADMIN_EMAIL"), + ExternalContentDisplay: os.Getenv("EXTERNAL_CONTENT_DISPLAY"), + } +} + +func initByEnv(env *Env) (err error) { + gin.SetMode(gin.TestMode) + if err = dbCheck(env); err != nil { + return err + } + if err = initConfigAndDb(env); err != nil { + return err + } + if err = initBaseInfo(env); err != nil { + return err + } + return nil +} + +func dbCheck(env *Env) (err error) { + req := &CheckDatabaseReq{ + DbType: env.DbType, + DbUsername: env.DbUsername, + DbPassword: env.DbPassword, + DbHost: env.DbHost, + DbName: env.DbName, + DbFile: env.DbFile, + } + return requestAPI(req, "POST", "/installation/db/check", CheckDatabase) +} + +func initConfigAndDb(env *Env) (err error) { + req := &CheckDatabaseReq{ + DbType: env.DbType, + DbUsername: env.DbUsername, + DbPassword: env.DbPassword, + DbHost: env.DbHost, + DbName: env.DbName, + DbFile: env.DbFile, + } + return requestAPI(req, "POST", "/installation/init", InitEnvironment) +} + +func initBaseInfo(env *Env) (err error) { + req := &InitBaseInfoReq{ + Language: env.Language, + SiteName: env.SiteName, + SiteURL: env.SiteURL, + ContactEmail: env.ContactEmail, + AdminName: env.AdminName, + AdminPassword: env.AdminPassword, + AdminEmail: env.AdminEmail, + LoginRequired: env.LoginRequired, + ExternalContentDisplay: env.ExternalContentDisplay, + } + return requestAPI(req, "POST", "/installation/base-info", InitBaseInfo) +} + +func requestAPI(req interface{}, method, url string, handlerFunc gin.HandlerFunc) error { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + body, _ := json.Marshal(req) + c.Request, _ = http.NewRequest(method, url, bytes.NewBuffer(body)) + if method == "POST" { + c.Request.Header.Set("Content-Type", "application/json") + } + handlerFunc(c) + if w.Code != http.StatusOK { + return fmt.Errorf(gjson.Get(w.Body.String(), "msg").String()) + } + return nil +} diff --git a/internal/install/install_main.go b/internal/install/install_main.go new file mode 100644 index 000000000..0d65f33f0 --- /dev/null +++ b/internal/install/install_main.go @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package install + +import ( + "fmt" + "os" + + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/cli" +) + +var ( + port = os.Getenv("INSTALL_PORT") + confPath = "" +) + +func Run(configPath string) { + confPath = configPath + // initialize translator for return internationalization error when installing. + _, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath}) + if err != nil { + panic(err) + } + + // try to install by env + if installByEnv, err := TryToInstallByEnv(); installByEnv && err != nil { + fmt.Printf("[auto-install] try to init by env fail: %v\n", err) + } + + installServer := NewInstallHTTPServer() + if len(port) == 0 { + port = "80" + } + fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port) + if err = installServer.Run(":" + port); err != nil { + panic(err) + } +} diff --git a/internal/install/install_req.go b/internal/install/install_req.go new file mode 100644 index 000000000..e5b8839e9 --- /dev/null +++ b/internal/install/install_req.go @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package install + +import ( + "fmt" + "net/url" + "strings" + + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/dir" + "github.com/segmentfault/pacman/errors" + "xorm.io/xorm/schemas" +) + +// CheckConfigFileResp check config file if exist or not response +type CheckConfigFileResp struct { + ConfigFileExist bool `json:"config_file_exist"` + DBConnectionSuccess bool `json:"db_connection_success"` + DbTableExist bool `json:"db_table_exist"` +} + +// CheckDatabaseReq check database +type CheckDatabaseReq struct { + DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"` + DbUsername string `json:"db_username"` + DbPassword string `json:"db_password"` + DbHost string `json:"db_host"` + DbName string `json:"db_name"` + DbFile string `json:"db_file"` + Ssl bool `json:"ssl_enabled"` + SslMode string `json:"ssl_mode"` + SslRootCert string `json:"ssl_root_cert"` + SslKey string `json:"ssl_key"` + SslCert string `json:"ssl_cert"` +} + +// GetConnection get connection string +func (r *CheckDatabaseReq) GetConnection() string { + if r.DbType == string(schemas.SQLITE) { + return r.DbFile + } + if r.DbType == string(schemas.MYSQL) { + return fmt.Sprintf("%s:%s@tcp(%s)/%s", + r.DbUsername, r.DbPassword, r.DbHost, r.DbName) + } + if r.DbType == string(schemas.POSTGRES) { + host, port := parsePgSQLHostPort(r.DbHost) + if !r.Ssl { + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, r.DbUsername, r.DbPassword, r.DbName) + } else if r.SslMode == "require" { + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode) + } else if r.SslMode == "verify-ca" || r.SslMode == "verify-full" { + connection := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode) + if len(r.SslRootCert) > 0 && dir.CheckFileExist(r.SslRootCert) { + connection += fmt.Sprintf(" sslrootcert=%s", r.SslRootCert) + } + if len(r.SslCert) > 0 && dir.CheckFileExist(r.SslCert) { + connection += fmt.Sprintf(" sslcert=%s", r.SslCert) + } + if len(r.SslKey) > 0 && dir.CheckFileExist(r.SslKey) { + connection += fmt.Sprintf(" sslkey=%s", r.SslKey) + } + return connection + } + } + return "" +} + +func parsePgSQLHostPort(dbHost string) (host string, port string) { + if strings.Contains(dbHost, ":") { + idx := strings.LastIndex(dbHost, ":") + host, port = dbHost[:idx], dbHost[idx+1:] + } else if len(dbHost) > 0 { + host = dbHost + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +// CheckDatabaseResp check database response +type CheckDatabaseResp struct { + ConnectionSuccess bool `json:"connection_success"` +} + +// InitEnvironmentResp init environment response +type InitEnvironmentResp struct { + Success bool `json:"success"` + CreateConfigFailed bool `json:"create_config_failed"` + DefaultConfig string `json:"default_config"` + ErrType string `json:"err_type"` +} + +// InitBaseInfoReq init base info request +type InitBaseInfoReq struct { + Language string `validate:"required,gt=0,lte=30" json:"lang"` + SiteName string `validate:"required,sanitizer,gt=0,lte=30" json:"site_name"` + SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"` + ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"` + AdminName string `validate:"required,gte=2,lte=30" json:"name"` + AdminPassword string `validate:"required,gte=8,lte=32" json:"password"` + AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"` + LoginRequired bool `json:"login_required"` + ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` +} + +func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) { + if checker.IsInvalidUsername(r.AdminName) { + errField := &validator.FormErrorField{ + ErrorField: "name", + ErrorMsg: reason.UsernameInvalid, + } + errFields = append(errFields, errField) + return errFields, errors.BadRequest(reason.UsernameInvalid) + } + return +} + +func (r *InitBaseInfoReq) FormatSiteUrl() { + parsedUrl, err := url.Parse(r.SiteURL) + if err != nil { + return + } + r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host) + if len(parsedUrl.Path) > 0 { + r.SiteURL = r.SiteURL + parsedUrl.Path + r.SiteURL = strings.TrimSuffix(r.SiteURL, "/") + } +} diff --git a/internal/install/install_server.go b/internal/install/install_server.go new file mode 100644 index 000000000..87517ec09 --- /dev/null +++ b/internal/install/install_server.go @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package install + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + + "github.com/apache/answer/configs" + "github.com/apache/answer/internal/base/conf" + "github.com/apache/answer/ui" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" + "gopkg.in/yaml.v3" +) + +const UIStaticPath = "build/static" + +type _resource struct { + fs embed.FS +} + +// Open to implement the interface by http.FS required +func (r *_resource) Open(name string) (fs.File, error) { + name = fmt.Sprintf(UIStaticPath+"/%s", name) + log.Debugf("open static path %s", name) + return r.fs.Open(name) +} + +// NewInstallHTTPServer new install http server. +func NewInstallHTTPServer() *gin.Engine { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + + c := &conf.AllConfig{} + _ = yaml.Unmarshal(configs.Config, c) + + r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) + r.StaticFS(c.UI.BaseURL+"/static", http.FS(&_resource{ + fs: ui.Build, + })) + + // read default config file and extract ui config + installApi := r.Group("") + installApi.GET(c.UI.BaseURL+"/", CheckConfigFileAndRedirectToInstallPage) + installApi.GET(c.UI.BaseURL+"/install", WebPage) + installApi.GET(c.UI.BaseURL+"/50x", WebPage) + installApi.GET(c.UI.APIBaseURL+"/installation/language/config", GetLangMapping) + installApi.GET(c.UI.APIBaseURL+"/installation/language/options", LangOptions) + installApi.POST(c.UI.APIBaseURL+"/installation/db/check", CheckDatabase) + installApi.POST(c.UI.APIBaseURL+"/installation/config-file/check", CheckConfigFile) + installApi.POST(c.UI.APIBaseURL+"/installation/init", InitEnvironment) + installApi.POST(c.UI.APIBaseURL+"/installation/base-info", InitBaseInfo) + + r.NoRoute(func(ctx *gin.Context) { + ctx.Redirect(http.StatusFound, "/50x") + }) + return r +} + +func WebPage(c *gin.Context) { + filePath := "" + var file []byte + var err error + filePath = "build/index.html" + c.Header("content-type", "text/html;charset=utf-8") + file, err = ui.Build.ReadFile(filePath) + if err != nil { + log.Error(err) + c.Status(http.StatusNotFound) + return + } + c.String(http.StatusOK, string(file)) +} diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0601ffdd7..fde27cdde 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -1,185 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package migrations import ( + "context" + "encoding/json" "fmt" + "time" + + "github.com/apache/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/entity" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/repo/unique" + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/internal/entity" + "golang.org/x/crypto/bcrypt" "xorm.io/xorm" ) -var ( - tables = []interface{}{ - &entity.Activity{}, - &entity.Answer{}, - &entity.Collection{}, - &entity.CollectionGroup{}, - &entity.Comment{}, - &entity.Config{}, - &entity.Meta{}, - &entity.Notification{}, - &entity.Question{}, - &entity.Report{}, - &entity.Revision{}, - &entity.SiteInfo{}, - &entity.Tag{}, - &entity.TagRel{}, - &entity.Uniqid{}, - &entity.User{}, - &entity.Version{}, +type Mentor struct { + ctx context.Context + engine *xorm.Engine + userData *InitNeedUserInputData + err error + Done bool +} + +func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor { + return &Mentor{ctx: ctx, engine: engine, userData: data} +} + +type InitNeedUserInputData struct { + Language string + SiteName string + SiteURL string + ContactEmail string + AdminName string + AdminPassword string + AdminEmail string + LoginRequired bool + ExternalContentDisplay string +} + +func (m *Mentor) InitDB() error { + m.do("check table exist", m.checkTableExist) + m.do("sync table", m.syncTable) + m.do("init version table", m.initVersionTable) + m.do("init admin user", m.initAdminUser) + m.do("init config", m.initConfig) + m.do("init default privileges config", m.initDefaultRankPrivileges) + m.do("init role", m.initRole) + m.do("init power", m.initPower) + m.do("init role power rel", m.initRolePowerRel) + m.do("init admin user role rel", m.initAdminUserRoleRel) + m.do("init site info interface", m.initSiteInfoInterface) + m.do("init site info general config", m.initSiteInfoGeneralData) + m.do("init site info login config", m.initSiteInfoLoginConfig) + m.do("init site info theme config", m.initSiteInfoThemeConfig) + m.do("init site info seo config", m.initSiteInfoSEOConfig) + m.do("init site info user config", m.initSiteInfoUsersConfig) + m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank) + m.do("init site info write", m.initSiteInfoWrite) + m.do("init site info legal", m.initSiteInfoLegalConfig) + m.do("init default content", m.initDefaultContent) + m.do("init default badges", m.initDefaultBadges) + return m.err +} + +func (m *Mentor) do(taskName string, fn func()) { + if m.err != nil || m.Done { + return } -) + fn() + if m.err != nil { + m.err = fmt.Errorf("%s failed: %s", taskName, m.err) + } +} -// InitDB init db -func InitDB(dataConf *data.Database) (err error) { - engine, err := data.NewDB(false, dataConf) - if err != nil { - fmt.Println("new database failed: ", err.Error()) - return err +func (m *Mentor) checkTableExist() { + m.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{}) + if m.Done { + fmt.Println("[database] already exists") } +} - exist, err := engine.IsTableExist(&entity.Version{}) - if err != nil { - return fmt.Errorf("check table exists failed: %s", err) +func (m *Mentor) syncTable() { + m.err = m.engine.Context(m.ctx).Sync(tables...) +} + +func (m *Mentor) initVersionTable() { + _, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()}) +} + +func (m *Mentor) initAdminUser() { + generateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.User{ + ID: "1", + Username: m.userData.AdminName, + Pass: string(generateFromPassword), + EMail: m.userData.AdminEmail, + MailStatus: 1, + NoticeStatus: 1, + Status: 1, + Rank: 1, + DisplayName: m.userData.AdminName, + }) +} + +func (m *Mentor) initConfig() { + _, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable) +} + +func (m *Mentor) initDefaultRankPrivileges() { + chooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2) + for _, privilege := range chooseOption.Privileges { + _, err := m.engine.Context(m.ctx).Update( + &entity.Config{Value: fmt.Sprintf("%d", privilege.Value)}, + &entity.Config{Key: privilege.Key}, + ) + if err != nil { + log.Error(err) + } } - if exist { - fmt.Println("[database] already exists") - return nil +} + +func (m *Mentor) initRole() { + _, m.err = m.engine.Context(m.ctx).Insert(roles) +} + +func (m *Mentor) initPower() { + _, m.err = m.engine.Context(m.ctx).Insert(powers) +} + +func (m *Mentor) initRolePowerRel() { + _, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels) +} + +func (m *Mentor) initAdminUserRoleRel() { + _, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel) +} + +func (m *Mentor) initSiteInfoInterface() { + now := time.Now() + zoneName, offset := now.In(time.Local).Zone() + + localTimezone := "UTC" + for _, tz := range constant.Timezones { + loc, err := time.LoadLocation(tz) + if err != nil { + continue + } + + tzNow := now.In(loc) + tzName, tzOffset := tzNow.Zone() + + if tzName == zoneName && tzOffset == offset { + localTimezone = tz + break + } + } + + interfaceData := map[string]string{ + "language": m.userData.Language, + "time_zone": localTimezone, + "default_avatar": "gravatar", + "gravatar_base_url": "https://www.gravatar.com/avatar/", + } + interfaceDataBytes, _ := json.Marshal(interfaceData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "interface", + Content: string(interfaceDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoGeneralData() { + generalData := map[string]string{ + "name": m.userData.SiteName, + "site_url": m.userData.SiteURL, + "contact_email": m.userData.ContactEmail, + } + generalDataBytes, _ := json.Marshal(generalData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "general", + Content: string(generalDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoLoginConfig() { + loginConfig := map[string]interface{}{ + "allow_new_registrations": true, + "allow_email_registrations": true, + "allow_password_login": true, + "login_required": m.userData.LoginRequired, } + loginConfigDataBytes, _ := json.Marshal(loginConfig) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "login", + Content: string(loginConfigDataBytes), + Status: 1, + }) +} - err = engine.Sync(tables...) +func (m *Mentor) initSiteInfoLegalConfig() { + legalConfig := map[string]interface{}{ + "external_content_display": m.userData.ExternalContentDisplay, + } + legalConfigDataBytes, _ := json.Marshal(legalConfig) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "legal", + Content: string(legalConfigDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoThemeConfig() { + themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}}}` + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "theme", + Content: themeConfig, + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoSEOConfig() { + seoData := map[string]interface{}{ + "permalink": constant.PermalinkQuestionID, + "robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml", + } + seoDataBytes, _ := json.Marshal(seoData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "seo", + Content: string(seoDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoUsersConfig() { + usersData := map[string]any{ + "default_avatar": "gravatar", + "gravatar_base_url": "https://www.gravatar.com/avatar/", + "allow_update_display_name": true, + "allow_update_username": true, + "allow_update_avatar": true, + "allow_update_bio": true, + "allow_update_website": true, + "allow_update_location": true, + } + usersDataBytes, _ := json.Marshal(usersData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "users", + Content: string(usersDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoPrivilegeRank() { + privilegeRankData := map[string]interface{}{ + "level": schema.PrivilegeLevel2, + } + privilegeRankDataBytes, _ := json.Marshal(privilegeRankData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "privileges", + Content: string(privilegeRankDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initSiteInfoWrite() { + writeData := map[string]interface{}{ + "restrict_answer": true, + "required_tag": false, + "recommend_tags": []string{}, + "reserved_tags": []string{}, + "max_image_size": 4, + "max_attachment_size": 8, + "max_image_megapixel": 40, + "authorized_image_extensions": []string{"jpg", "jpeg", "png", "gif", "webp"}, + "authorized_attachment_extensions": []string{}, + } + writeDataBytes, _ := json.Marshal(writeData) + _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{ + Type: "write", + Content: string(writeDataBytes), + Status: 1, + }) +} + +func (m *Mentor) initDefaultContent() { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) + now := time.Now() + + tagId, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Tag{}.TableName()) if err != nil { - return fmt.Errorf("sync table failed: %s", err) + m.err = err + return } - err = initAdminUser(engine) + q1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName()) if err != nil { - return fmt.Errorf("init admin user failed: %s", err) + m.err = err + return } - err = initSiteInfo(engine) + a1Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName()) if err != nil { - return fmt.Errorf("init site info failed: %s", err) + m.err = err + return } - err = initConfigTable(engine) + q2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Question{}.TableName()) if err != nil { - return fmt.Errorf("init config table: %s", err) + m.err = err + return } - return nil -} -func initAdminUser(engine *xorm.Engine) error { - _, err := engine.InsertOne(&entity.User{ - Username: "admin", - Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin - EMail: "admin@admin.com", - MailStatus: 1, - NoticeStatus: 1, - Status: 1, - DisplayName: "admin", - IsAdmin: true, + a2Id, err := uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Answer{}.TableName()) + if err != nil { + m.err = err + return + } + + tag := entity.Tag{ + ID: tagId, + SlugName: "support", + DisplayName: "support", + OriginalText: "For general support questions.", + ParsedText: "

For general support questions.

", + UserID: "1", + QuestionCount: 2, + Status: entity.TagStatusAvailable, + RevisionID: "0", + } + + q1 := &entity.Question{ + ID: q1Id, + CreatedAt: now, + UserID: "1", + LastEditUserID: "1", + Title: "What is a tag?", + OriginalText: "When asking a question, we need to choose tags. What are tags and why should I use them?", + ParsedText: "

When asking a question, we need to choose tags. What are tags and why should I use them?

", + Pin: entity.QuestionUnPin, + Show: entity.QuestionShow, + Status: entity.QuestionStatusAvailable, + AnswerCount: 1, + AcceptedAnswerID: "0", + LastAnswerID: a1Id, + PostUpdateTime: now, + RevisionID: "0", + } + + a1 := &entity.Answer{ + ID: a1Id, + CreatedAt: now, + QuestionID: q1Id, + UserID: "1", + LastEditUserID: "0", + OriginalText: "Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.", + ParsedText: "

Tags help to organize content and make searching easier. It helps your question get more attention from people interested in that tag. Tags also send notifications. If you are interested in some topic, follow that tag to get updates.

", + Status: entity.AnswerStatusAvailable, + RevisionID: "0", + } + + q2 := &entity.Question{ + ID: q2Id, + CreatedAt: now, + UserID: "1", + LastEditUserID: "1", + Title: "What is reputation and how do I earn them?", + OriginalText: "I see that each user has reputation points, What is it and how do I earn them?", + ParsedText: "

I see that each user has reputation points, What is it and how do I earn them?

", + Pin: entity.QuestionUnPin, + Show: entity.QuestionShow, + Status: entity.QuestionStatusAvailable, + AnswerCount: 1, + AcceptedAnswerID: "0", + LastAnswerID: a2Id, + PostUpdateTime: now, + RevisionID: "0", + } + + a2 := &entity.Answer{ + ID: a2Id, + CreatedAt: now, + QuestionID: q2Id, + UserID: "1", + LastEditUserID: "0", + OriginalText: "Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.", + ParsedText: "

Your reputation points show how much the community values your knowledge. You earn points when someone find your question or answer helpful. You also get points when the person who asked the question thinks you did a good job and accepts your answer.

", + Status: entity.AnswerStatusAvailable, + RevisionID: "0", + } + + _, m.err = m.engine.Context(m.ctx).Insert(tag) + if m.err != nil { + return + } + + _, m.err = m.engine.Context(m.ctx).Insert(q1) + if m.err != nil { + return + } + + _, m.err = m.engine.Context(m.ctx).Insert(a1) + if m.err != nil { + return + } + + _, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{ + ObjectID: q1.ID, + TagID: tag.ID, + Status: entity.TagRelStatusAvailable, }) - return err -} + if m.err != nil { + return + } -func initSiteInfo(engine *xorm.Engine) error { - _, err := engine.InsertOne(&entity.SiteInfo{ - Type: "interface", - Content: `{"logo":"","theme":"black","language":"en_US"}`, - Status: 1, + _, m.err = m.engine.Context(m.ctx).Insert(q2) + if m.err != nil { + return + } + + _, m.err = m.engine.Context(m.ctx).Insert(a2) + if m.err != nil { + return + } + + _, m.err = m.engine.Context(m.ctx).Insert(entity.TagRel{ + ObjectID: q2.ID, + TagID: tag.ID, + Status: entity.TagRelStatusAvailable, }) - return err -} - -func initConfigTable(engine *xorm.Engine) error { - defaultConfigTable := []*entity.Config{ - {1, "answer.accepted", `15`}, - {2, "answer.voted_up", `10`}, - {3, "question.voted_up", `10`}, - {4, "tag.edit_accepted", `2`}, - {5, "answer.accept", `2`}, - {6, "answer.voted_down_cancel", `2`}, - {7, "question.voted_down_cancel", `2`}, - {8, "answer.vote_down_cancel", `1`}, - {9, "question.vote_down_cancel", `1`}, - {10, "user.activated", `1`}, - {11, "edit.accepted", `2`}, - {12, "answer.vote_down", `-1`}, - {13, "question.voted_down", `-2`}, - {14, "answer.voted_down", `-2`}, - {15, "answer.accept_cancel", `-2`}, - {16, "answer.deleted", `-5`}, - {17, "question.voted_up_cancel", `-10`}, - {18, "answer.voted_up_cancel", `-10`}, - {19, "answer.accepted_cancel", `-15`}, - {20, "object.reported", `-100`}, - {21, "edit.rejected", `-2`}, - {22, "daily_rank_limit", `200`}, - {23, "daily_rank_limit.exclude", `["answer.accepted"]`}, - {24, "user.follow", `0`}, - {25, "comment.vote_up", `0`}, - {26, "comment.vote_up_cancel", `0`}, - {27, "question.vote_down", `0`}, - {28, "question.vote_up", `0`}, - {29, "question.vote_up_cancel", `0`}, - {30, "answer.vote_up", `0`}, - {31, "answer.vote_up_cancel", `0`}, - {32, "question.follow", `0`}, - {33, "email.config", `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, - {35, "tag.follow", `0`}, - {36, "rank.question.add", `0`}, - {37, "rank.question.edit", `0`}, - {38, "rank.question.delete", `0`}, - {39, "rank.question.vote_up", `0`}, - {40, "rank.question.vote_down", `0`}, - {41, "rank.answer.add", `0`}, - {42, "rank.answer.edit", `0`}, - {43, "rank.answer.delete", `0`}, - {44, "rank.answer.accept", `0`}, - {45, "rank.answer.vote_up", `0`}, - {46, "rank.answer.vote_down", `0`}, - {47, "rank.comment.add", `0`}, - {48, "rank.comment.edit", `0`}, - {49, "rank.comment.delete", `0`}, - {50, "rank.report.add", `0`}, - {51, "rank.tag.add", `0`}, - {52, "rank.tag.edit", `0`}, - {53, "rank.tag.delete", `0`}, - {54, "rank.tag.synonym", `0`}, - {55, "rank.link.url_limit", `0`}, - {56, "rank.vote.detail", `0`}, - {57, "reason.spam", `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`}, - {58, "reason.rude_or_abusive", `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`}, - {59, "reason.something", `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, - {60, "reason.a_duplicate", `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, - {61, "reason.not_a_answer", `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, - {62, "reason.no_longer_needed", `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, - {63, "reason.community_specific", `{"name":"a community-specific reason","description":"This question doesn’t meet a community guideline."}`}, - {64, "reason.not_clarity", `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, - {65, "reason.normal", `{"name":"normal","description":"A normal post available to everyone."}`}, - {66, "reason.normal.user", `{"name":"normal","description":"A normal user can ask and answer questions."}`}, - {67, "reason.closed", `{"name":"closed","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {68, "reason.deleted", `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, - {69, "reason.deleted.user", `{"name":"deleted","description":"Delete profile, authentication associations."}`}, - {70, "reason.suspended", `{"name":"suspended","description":"A suspended user can’t log in."}`}, - {71, "reason.inactive", `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, - {72, "reason.looks_ok", `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, - {73, "reason.needs_edit", `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, - {74, "reason.needs_close", `{"name":"needs close","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {75, "reason.needs_delete", `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, - {76, "question.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, - {77, "answer.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, - {78, "comment.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, - {79, "question.close.reasons", `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, - {80, "question.status.reasons", `["reason.normal","reason.closed","reason.deleted"]`}, - {81, "answer.status.reasons", `["reason.normal","reason.deleted"]`}, - {82, "comment.status.reasons", `["reason.normal","reason.deleted"]`}, - {83, "user.status.reasons", `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, - {84, "question.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, - {85, "answer.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, - {86, "comment.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, - } - _, err := engine.Insert(defaultConfigTable) - return err + if m.err != nil { + return + } +} + +func (m *Mentor) initDefaultBadges() { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) + + _, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable) + if m.err != nil { + return + } + for _, badge := range defaultBadgeTable { + badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName()) + if m.err != nil { + return + } + if _, m.err = m.engine.Context(m.ctx).Insert(badge); m.err != nil { + return + } + } } diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go new file mode 100644 index 000000000..96151625d --- /dev/null +++ b/internal/migrations/init_data.go @@ -0,0 +1,510 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/permission" +) + +const ( + defaultSEORobotTxt = `User-agent: * +Disallow: /admin +Disallow: /search +Disallow: /install +Disallow: /review +Disallow: /users/login +Disallow: /users/register +Disallow: /users/account-recovery +Disallow: /users/oauth/* +Disallow: /users/*/* +Disallow: /answer/api +Disallow: /*?code* +Disallow: /swagger/* + +Sitemap: ` +) + +var ( + tables = []interface{}{ + &entity.Activity{}, + &entity.Answer{}, + &entity.Collection{}, + &entity.CollectionGroup{}, + &entity.Comment{}, + &entity.Config{}, + &entity.Meta{}, + &entity.Notification{}, + &entity.Question{}, + &entity.QuestionLink{}, + &entity.Report{}, + &entity.Revision{}, + &entity.SiteInfo{}, + &entity.Tag{}, + &entity.TagRel{}, + &entity.Uniqid{}, + &entity.User{}, + &entity.Version{}, + &entity.Role{}, + &entity.RolePowerRel{}, + &entity.Power{}, + &entity.UserRoleRel{}, + &entity.PluginConfig{}, + &entity.UserExternalLogin{}, + &entity.UserNotificationConfig{}, + &entity.PluginUserConfig{}, + &entity.Review{}, + &entity.Badge{}, + &entity.BadgeGroup{}, + &entity.BadgeAward{}, + &entity.FileRecord{}, + &entity.PluginKVStorage{}, + } + + roles = []*entity.Role{ + {ID: 1, Name: "User", Description: "Default with no special access."}, + {ID: 2, Name: "Admin", Description: "Have the full power to access the site."}, + {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."}, + } + + powers = []*entity.Power{ + {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"}, + {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"}, + {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"}, + {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"}, + {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"}, + {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"}, + {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"}, + {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"}, + {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"}, + {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"}, + {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"}, + {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"}, + {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"}, + {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"}, + {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"}, + {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"}, + {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"}, + {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"}, + {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"}, + {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"}, + {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"}, + {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"}, + {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"}, + {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"}, + {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"}, + {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"}, + {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"}, + {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"}, + {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"}, + {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"}, + {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"}, + {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"}, + {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"}, + {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, + {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, + {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, + {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, + {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, + {ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"}, + {ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"}, + {ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"}, + } + + rolePowerRels = []*entity.RolePowerRel{ + {RoleID: 2, PowerType: permission.AdminAccess}, + {RoleID: 2, PowerType: permission.QuestionAdd}, + {RoleID: 2, PowerType: permission.QuestionEdit}, + {RoleID: 2, PowerType: permission.QuestionEditWithoutReview}, + {RoleID: 2, PowerType: permission.QuestionDelete}, + {RoleID: 2, PowerType: permission.QuestionClose}, + {RoleID: 2, PowerType: permission.QuestionReopen}, + {RoleID: 2, PowerType: permission.QuestionVoteUp}, + {RoleID: 2, PowerType: permission.QuestionVoteDown}, + {RoleID: 2, PowerType: permission.AnswerAdd}, + {RoleID: 2, PowerType: permission.AnswerEdit}, + {RoleID: 2, PowerType: permission.AnswerEditWithoutReview}, + {RoleID: 2, PowerType: permission.AnswerDelete}, + {RoleID: 2, PowerType: permission.AnswerAccept}, + {RoleID: 2, PowerType: permission.AnswerVoteUp}, + {RoleID: 2, PowerType: permission.AnswerVoteDown}, + {RoleID: 2, PowerType: permission.CommentAdd}, + {RoleID: 2, PowerType: permission.CommentEdit}, + {RoleID: 2, PowerType: permission.CommentDelete}, + {RoleID: 2, PowerType: permission.CommentVoteUp}, + {RoleID: 2, PowerType: permission.CommentVoteDown}, + {RoleID: 2, PowerType: permission.ReportAdd}, + {RoleID: 2, PowerType: permission.TagAdd}, + {RoleID: 2, PowerType: permission.TagEdit}, + {RoleID: 2, PowerType: permission.TagEditSlugName}, + {RoleID: 2, PowerType: permission.TagEditWithoutReview}, + {RoleID: 2, PowerType: permission.TagDelete}, + {RoleID: 2, PowerType: permission.TagSynonym}, + {RoleID: 2, PowerType: permission.LinkUrlLimit}, + {RoleID: 2, PowerType: permission.VoteDetail}, + {RoleID: 2, PowerType: permission.AnswerAudit}, + {RoleID: 2, PowerType: permission.QuestionAudit}, + {RoleID: 2, PowerType: permission.TagAudit}, + {RoleID: 2, PowerType: permission.TagUseReservedTag}, + {RoleID: 2, PowerType: permission.QuestionPin}, + {RoleID: 2, PowerType: permission.QuestionHide}, + {RoleID: 2, PowerType: permission.QuestionUnPin}, + {RoleID: 2, PowerType: permission.QuestionShow}, + {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, + {RoleID: 2, PowerType: permission.AnswerUnDelete}, + {RoleID: 2, PowerType: permission.QuestionUnDelete}, + {RoleID: 2, PowerType: permission.TagUnDelete}, + + {RoleID: 3, PowerType: permission.QuestionAdd}, + {RoleID: 3, PowerType: permission.QuestionEdit}, + {RoleID: 3, PowerType: permission.QuestionEditWithoutReview}, + {RoleID: 3, PowerType: permission.QuestionDelete}, + {RoleID: 3, PowerType: permission.QuestionClose}, + {RoleID: 3, PowerType: permission.QuestionReopen}, + {RoleID: 3, PowerType: permission.QuestionVoteUp}, + {RoleID: 3, PowerType: permission.QuestionVoteDown}, + {RoleID: 3, PowerType: permission.AnswerAdd}, + {RoleID: 3, PowerType: permission.AnswerEdit}, + {RoleID: 3, PowerType: permission.AnswerEditWithoutReview}, + {RoleID: 3, PowerType: permission.AnswerDelete}, + {RoleID: 3, PowerType: permission.AnswerAccept}, + {RoleID: 3, PowerType: permission.AnswerVoteUp}, + {RoleID: 3, PowerType: permission.AnswerVoteDown}, + {RoleID: 3, PowerType: permission.CommentAdd}, + {RoleID: 3, PowerType: permission.CommentEdit}, + {RoleID: 3, PowerType: permission.CommentDelete}, + {RoleID: 3, PowerType: permission.CommentVoteUp}, + {RoleID: 3, PowerType: permission.CommentVoteDown}, + {RoleID: 3, PowerType: permission.ReportAdd}, + {RoleID: 3, PowerType: permission.TagAdd}, + {RoleID: 3, PowerType: permission.TagEdit}, + {RoleID: 3, PowerType: permission.TagEditSlugName}, + {RoleID: 3, PowerType: permission.TagEditWithoutReview}, + {RoleID: 3, PowerType: permission.TagDelete}, + {RoleID: 3, PowerType: permission.TagSynonym}, + {RoleID: 3, PowerType: permission.LinkUrlLimit}, + {RoleID: 3, PowerType: permission.VoteDetail}, + {RoleID: 3, PowerType: permission.AnswerAudit}, + {RoleID: 3, PowerType: permission.QuestionAudit}, + {RoleID: 3, PowerType: permission.TagAudit}, + {RoleID: 3, PowerType: permission.TagUseReservedTag}, + {RoleID: 3, PowerType: permission.QuestionPin}, + {RoleID: 3, PowerType: permission.QuestionHide}, + {RoleID: 3, PowerType: permission.QuestionUnPin}, + {RoleID: 3, PowerType: permission.QuestionShow}, + {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, + {RoleID: 3, PowerType: permission.AnswerUnDelete}, + {RoleID: 3, PowerType: permission.QuestionUnDelete}, + {RoleID: 3, PowerType: permission.TagUnDelete}, + } + + adminUserRoleRel = &entity.UserRoleRel{ + UserID: "1", + RoleID: 2, + } + + defaultConfigTable = []*entity.Config{ + {ID: 1, Key: "answer.accepted", Value: `15`}, + {ID: 2, Key: "answer.voted_up", Value: `10`}, + {ID: 3, Key: "question.voted_up", Value: `10`}, + {ID: 4, Key: "tag.edit_accepted", Value: `2`}, + {ID: 5, Key: "answer.accept", Value: `2`}, + {ID: 6, Key: "answer.voted_down_cancel", Value: `2`}, + {ID: 7, Key: "question.voted_down_cancel", Value: `2`}, + {ID: 8, Key: "answer.vote_down_cancel", Value: `1`}, + {ID: 9, Key: "question.vote_down_cancel", Value: `1`}, + {ID: 10, Key: "user.activated", Value: `1`}, + {ID: 11, Key: "edit.accepted", Value: `2`}, + {ID: 12, Key: "answer.vote_down", Value: `-1`}, + {ID: 13, Key: "question.voted_down", Value: `-2`}, + {ID: 14, Key: "answer.voted_down", Value: `-2`}, + {ID: 15, Key: "answer.accept_cancel", Value: `-2`}, + {ID: 16, Key: "answer.deleted", Value: `-5`}, + {ID: 17, Key: "question.voted_up_cancel", Value: `-10`}, + {ID: 18, Key: "answer.voted_up_cancel", Value: `-10`}, + {ID: 19, Key: "answer.accepted_cancel", Value: `-15`}, + {ID: 20, Key: "object.reported", Value: `-100`}, + {ID: 21, Key: "edit.rejected", Value: `-2`}, + {ID: 22, Key: "daily_rank_limit", Value: `200`}, + {ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`}, + {ID: 24, Key: "user.follow", Value: `0`}, + {ID: 25, Key: "comment.vote_up", Value: `0`}, + {ID: 26, Key: "comment.vote_up_cancel", Value: `0`}, + {ID: 27, Key: "question.vote_down", Value: `0`}, + {ID: 28, Key: "question.vote_up", Value: `0`}, + {ID: 29, Key: "question.vote_up_cancel", Value: `0`}, + {ID: 30, Key: "answer.vote_up", Value: `0`}, + {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, + {ID: 32, Key: "question.follow", Value: `0`}, + {ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`}, + {ID: 35, Key: "tag.follow", Value: `0`}, + {ID: 36, Key: "rank.question.add", Value: `1`}, + {ID: 37, Key: "rank.question.edit", Value: `200`}, + {ID: 38, Key: "rank.question.delete", Value: `-1`}, + {ID: 39, Key: "rank.question.vote_up", Value: `15`}, + {ID: 40, Key: "rank.question.vote_down", Value: `125`}, + {ID: 41, Key: "rank.answer.add", Value: `1`}, + {ID: 42, Key: "rank.answer.edit", Value: `200`}, + {ID: 43, Key: "rank.answer.delete", Value: `-1`}, + {ID: 44, Key: "rank.answer.accept", Value: `-1`}, + {ID: 45, Key: "rank.answer.vote_up", Value: `15`}, + {ID: 46, Key: "rank.answer.vote_down", Value: `125`}, + {ID: 47, Key: "rank.comment.add", Value: `1`}, + {ID: 48, Key: "rank.comment.edit", Value: `-1`}, + {ID: 49, Key: "rank.comment.delete", Value: `-1`}, + {ID: 50, Key: "rank.report.add", Value: `1`}, + {ID: 51, Key: "rank.tag.add", Value: `1500`}, + {ID: 52, Key: "rank.tag.edit", Value: `100`}, + {ID: 53, Key: "rank.tag.delete", Value: `-1`}, + {ID: 54, Key: "rank.tag.synonym", Value: `20000`}, + {ID: 55, Key: "rank.link.url_limit", Value: `10`}, + {ID: 56, Key: "rank.vote.detail", Value: `0`}, + {ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`}, + {ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`}, + {ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, + {ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, + {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, + {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, + {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn't meet a community guideline."}`}, + {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`}, + {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, + {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, + {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can't answer, but still can edit, vote and comment."}`}, + {ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, + {ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`}, + {ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can't log in."}`}, + {ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, + {ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, + {ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, + {ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can't answer, but still can edit, vote and comment."}`}, + {ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, + {ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, + {ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, + {ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, + {ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, + {ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`}, + {ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, + {ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, + {ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 87, Key: "question.asked", Value: `0`}, + {ID: 88, Key: "question.closed", Value: `0`}, + {ID: 89, Key: "question.reopened", Value: `0`}, + {ID: 90, Key: "question.answered", Value: `0`}, + {ID: 91, Key: "question.commented", Value: `0`}, + {ID: 92, Key: "question.accept", Value: `0`}, + {ID: 93, Key: "question.edited", Value: `0`}, + {ID: 94, Key: "question.rollback", Value: `0`}, + {ID: 95, Key: "question.deleted", Value: `0`}, + {ID: 96, Key: "question.undeleted", Value: `0`}, + {ID: 97, Key: "answer.answered", Value: `0`}, + {ID: 98, Key: "answer.commented", Value: `0`}, + {ID: 99, Key: "answer.edited", Value: `0`}, + {ID: 100, Key: "answer.rollback", Value: `0`}, + {ID: 101, Key: "answer.undeleted", Value: `0`}, + {ID: 102, Key: "tag.created", Value: `0`}, + {ID: 103, Key: "tag.edited", Value: `0`}, + {ID: 104, Key: "tag.rollback", Value: `0`}, + {ID: 105, Key: "tag.deleted", Value: `0`}, + {ID: 106, Key: "tag.undeleted", Value: `0`}, + {ID: 107, Key: "rank.comment.vote_up", Value: `1`}, + {ID: 108, Key: "rank.comment.vote_down", Value: `1`}, + {ID: 109, Key: "rank.question.edit_without_review", Value: `2000`}, + {ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`}, + {ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`}, + {ID: 112, Key: "rank.answer.audit", Value: `2000`}, + {ID: 113, Key: "rank.question.audit", Value: `2000`}, + {ID: 114, Key: "rank.tag.audit", Value: `20000`}, + {ID: 115, Key: "rank.question.close", Value: `-1`}, + {ID: 116, Key: "rank.question.reopen", Value: `-1`}, + {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`}, + {ID: 118, Key: "plugin.status", Value: `{}`}, + {ID: 119, Key: "question.pin", Value: `0`}, + {ID: 120, Key: "question.unpin", Value: `0`}, + {ID: 121, Key: "question.show", Value: `0`}, + {ID: 122, Key: "question.hide", Value: `0`}, + {ID: 123, Key: "rank.question.pin", Value: `-1`}, + {ID: 124, Key: "rank.question.unpin", Value: `-1`}, + {ID: 125, Key: "rank.question.show", Value: `-1`}, + {ID: 126, Key: "rank.question.hide", Value: `-1`}, + {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, + {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, + {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, + {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, + } + + defaultBadgeGroupTable = []*entity.BadgeGroup{ + {ID: "1", Name: "badge.default_badge_groups.getting_started.name"}, + {ID: "2", Name: "badge.default_badge_groups.community.name"}, + {ID: "3", Name: "badge.default_badge_groups.posting.name"}, + } + + defaultBadgeTable = []*entity.Badge{ + { + Name: "badge.default_badges.autobiographer.name", + Icon: "person-badge-fill", + Description: "badge.default_badges.autobiographer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstUpdateUserProfile", + }, + { + Name: "badge.default_badges.editor.name", + Icon: "pencil-fill", + Description: "badge.default_badges.editor.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstPostEdit", + }, + { + Name: "badge.default_badges.first_flag.name", + Icon: "flag-fill", + Description: "badge.default_badges.first_flag.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstFlaggedPost", + }, + { + Name: "badge.default_badges.first_upvote.name", + Icon: "hand-thumbs-up-fill", + Description: "badge.default_badges.first_upvote.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstVotedPost", + }, + { + Name: "badge.default_badges.first_reaction.name", + Icon: "emoji-smile-fill", + Description: "badge.default_badges.first_reaction.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstReactedPost", + }, + { + Name: "badge.default_badges.first_share.name", + Icon: "share-fill", + Description: "badge.default_badges.first_share.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstSharedPost", + }, + { + Name: "badge.default_badges.scholar.name", + Icon: "check-circle-fill", + Description: "badge.default_badges.scholar.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstAcceptAnswer", + }, + { + Name: "badge.default_badges.solved.name", + Icon: "check-square-fill", + Description: "badge.default_badges.solved.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 2, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "ReachAnswerAcceptedAmount", + Param: `{"amount":"1"}`, + }, + { + Name: "badge.default_badges.nice_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.nice_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"10"}`, + }, + { + Name: "badge.default_badges.good_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.good_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelSilver, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"25"}`, + }, + { + Name: "badge.default_badges.great_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.great_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelGold, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"50"}`, + }, + { + Name: "badge.default_badges.nice_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.nice_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"10"}`, + }, + { + Name: "badge.default_badges.good_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.good_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelSilver, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"25"}`, + }, + { + Name: "badge.default_badges.great_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.great_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelGold, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"50"}`, + }, + } +) diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index f4f4efd8d..4b5335b06 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -1,25 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package migrations import ( + "context" "fmt" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/entity" - "github.com/segmentfault/pacman/log" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) -const minDBVersion = 0 // answer 1.0.0 +const minDBVersion = 0 // Migration describes on migration from lower version to high version type Migration interface { + Version() string Description() string - Migrate(*xorm.Engine) error + Migrate(ctx context.Context, x *xorm.Engine) error + ShouldCleanCache() bool } type migration struct { - description string - migrate func(*xorm.Engine) error + version string + description string + migrate func(ctx context.Context, x *xorm.Engine) error + shouldCleanCache bool +} + +// Version returns the migration's version +func (m *migration) Version() string { + return m.version } // Description returns the migration's description @@ -28,21 +56,57 @@ func (m *migration) Description() string { } // Migrate executes the migration -func (m *migration) Migrate(x *xorm.Engine) error { - return m.migrate(x) +func (m *migration) Migrate(ctx context.Context, x *xorm.Engine) error { + return m.migrate(ctx, x) +} + +// ShouldCleanCache should clean the cache +func (m *migration) ShouldCleanCache() bool { + return m.shouldCleanCache } // NewMigration creates a new migration -func NewMigration(desc string, fn func(*xorm.Engine) error) Migration { - return &migration{description: desc, migrate: fn} +func NewMigration(version, desc string, fn func(ctx context.Context, x *xorm.Engine) error, shouldCleanCache bool) Migration { + return &migration{version: version, description: desc, migrate: fn, shouldCleanCache: shouldCleanCache} } // Use noopMigration when there is a migration that has been no-oped -var noopMigration = func(_ *xorm.Engine) error { return nil } +var noopMigration = func(_ context.Context, _ *xorm.Engine) error { return nil } var migrations = []Migration{ // 0->1 - NewMigration("this is first version, no operation", noopMigration), + NewMigration("v0.0.1", "this is first version, no operation", noopMigration, false), + NewMigration("v0.3.0", "add user language", addUserLanguage, false), + NewMigration("v0.4.1", "add recommend and reserved tag fields", addTagRecommendedAndReserved, false), + NewMigration("v0.5.0", "add activity timeline", addActivityTimeline, false), + NewMigration("v0.6.0", "add user role", addRoleFeatures, false), + NewMigration("v1.0.0", "add theme and private mode", addThemeAndPrivateMode, true), + NewMigration("v1.0.2", "add new answer notification", addNewAnswerNotification, true), + NewMigration("v1.0.5", "add plugin", addPlugin, false), + NewMigration("v1.0.7", "add user pin hide features", addRolePinAndHideFeatures, true), + NewMigration("v1.0.8", "update accept answer rank", updateAcceptAnswerRank, true), + NewMigration("v1.0.9", "add login limitations", addLoginLimitations, true), + NewMigration("v1.1.0-beta.1", "update user pin hide features", updateRolePinAndHideFeatures, true), + NewMigration("v1.1.0-beta.2", "update question post time", updateQuestionPostTime, true), + NewMigration("v1.1.0", "add gravatar base url", updateCount, true), + NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false), + NewMigration("v1.1.2", "add notification config", addNoticeConfig, true), + NewMigration("v1.1.3", "set default user notification config", setDefaultUserNotificationConfig, false), + NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true), + NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true), + NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true), + NewMigration("v1.3.0", "add review", addReview, false), + NewMigration("v1.3.6", "add hot score to question table", addQuestionHotScore, true), + NewMigration("v1.4.0", "add badge/badge_group/badge_award table", addBadges, true), + NewMigration("v1.4.1", "add question link", addQuestionLink, true), + NewMigration("v1.4.2", "add the number of question links", addQuestionLinkedCount, true), + NewMigration("v1.4.5", "add file record", addFileRecord, true), + NewMigration("v1.5.1", "add plugin kv storage", addPluginKVStorage, true), + NewMigration("v1.6.0", "move user config to interface", moveUserConfigToInterface, true), +} + +func GetMigrations() []Migration { + return migrations } // GetCurrentDBVersion returns the current db version @@ -72,34 +136,56 @@ func ExpectedVersion() int64 { } // Migrate database to current version -func Migrate(dataConf *data.Database) error { - engine, err := data.NewDB(false, dataConf) +func Migrate(debug bool, dbConf *data.Database, cacheConf *data.CacheConf, upgradeToSpecificVersion string) error { + cache, cacheCleanup, err := data.NewCache(cacheConf) + if err != nil { + fmt.Println("new cache failed:", err.Error()) + } + engine, err := data.NewDB(debug, dbConf) if err != nil { fmt.Println("new database failed: ", err.Error()) return err } + defer engine.Close() currentDBVersion, err := GetCurrentDBVersion(engine) if err != nil { return err } expectedVersion := ExpectedVersion() + if len(upgradeToSpecificVersion) > 0 { + fmt.Printf("[migrate] user set upgrade to version: %s\n", upgradeToSpecificVersion) + for i, m := range migrations { + if m.Version() == upgradeToSpecificVersion { + currentDBVersion = int64(i) + break + } + } + } for currentDBVersion < expectedVersion { - log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d", + fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n", currentDBVersion, currentDBVersion+1, expectedVersion) migrationFunc := migrations[currentDBVersion] - log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description()) - if err := migrationFunc.Migrate(engine); err != nil { - log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] try to migrate Answer version %s, description: %s\n", migrationFunc.Version(), migrationFunc.Description()) + if err := migrationFunc.Migrate(context.Background(), engine); err != nil { + fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error()) return err } - log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1) + if migrationFunc.ShouldCleanCache() { + if err := cache.Flush(context.Background()); err != nil { + fmt.Printf("[migrate] flush cache failed: %s\n", err.Error()) + } + } + fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1) if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil { - log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) return err } currentDBVersion++ } + if cache != nil { + cacheCleanup() + } return nil } diff --git a/internal/migrations/v1.go b/internal/migrations/v1.go new file mode 100644 index 000000000..c5e731a0e --- /dev/null +++ b/internal/migrations/v1.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "xorm.io/xorm" +) + +func addUserLanguage(ctx context.Context, x *xorm.Engine) error { + type User struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"` + Language string `xorm:"not null default '' VARCHAR(100) language"` + } + return x.Context(ctx).Sync(new(User)) +} diff --git a/internal/migrations/v10.go b/internal/migrations/v10.go new file mode 100644 index 000000000..cb1b74994 --- /dev/null +++ b/internal/migrations/v10.go @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/tidwall/gjson" + "xorm.io/xorm" +) + +func addLoginLimitations(ctx context.Context, x *xorm.Engine) error { + loginSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeLogin, + } + exist, err := x.Context(ctx).Get(loginSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + content := &schema.SiteLoginReq{} + _ = json.Unmarshal([]byte(loginSiteInfo.Content), content) + content.AllowEmailRegistrations = true + content.AllowEmailDomains = make([]string, 0) + data, _ := json.Marshal(content) + loginSiteInfo.Content = string(data) + _, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + + interfaceSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeInterface, + } + exist, err = x.Context(ctx).Get(interfaceSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + siteUsers := &schema.SiteUsersReq{ + AllowUpdateDisplayName: true, + AllowUpdateUsername: true, + AllowUpdateAvatar: true, + AllowUpdateBio: true, + AllowUpdateWebsite: true, + AllowUpdateLocation: true, + } + if exist { + siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String() + } + data, _ := json.Marshal(siteUsers) + + exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: constant.SiteTypeUsers}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + usersSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeUsers, + Content: string(data), + Status: 1, + } + _, err = x.Context(ctx).Insert(usersSiteInfo) + if err != nil { + return fmt.Errorf("insert site info failed: %w", err) + } + } + return nil +} diff --git a/internal/migrations/v11.go b/internal/migrations/v11.go new file mode 100644 index 000000000..675690af0 --- /dev/null +++ b/internal/migrations/v11.go @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func updateRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { + defaultConfigTable := []*entity.Config{ + {ID: 119, Key: "question.pin", Value: `0`}, + {ID: 120, Key: "question.unpin", Value: `0`}, + {ID: 121, Key: "question.show", Value: `0`}, + {ID: 122, Key: "question.hide", Value: `0`}, + {ID: 123, Key: "rank.question.pin", Value: `-1`}, + {ID: 124, Key: "rank.question.unpin", Value: `-1`}, + {ID: 125, Key: "rank.question.show", Value: `-1`}, + {ID: 126, Key: "rank.question.hide", Value: `-1`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + + return nil +} diff --git a/internal/migrations/v12.go b/internal/migrations/v12.go new file mode 100644 index 000000000..2dd3ce71e --- /dev/null +++ b/internal/migrations/v12.go @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +type QuestionPostTime struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + Title string `xorm:"not null default '' VARCHAR(150) title"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` + ViewCount int `xorm:"not null default 0 INT(11) view_count"` + UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` + FollowCount int `xorm:"not null default 0 INT(11) follow_count"` + AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` + LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` +} + +func (QuestionPostTime) TableName() string { + return "question" +} + +func updateQuestionPostTime(ctx context.Context, x *xorm.Engine) error { + questionList := make([]QuestionPostTime, 0) + err := x.Context(ctx).Find(&questionList, &entity.Question{}) + if err != nil { + return fmt.Errorf("get questions failed: %w", err) + } + for _, item := range questionList { + if item.PostUpdateTime.IsZero() { + if !item.UpdatedAt.IsZero() { + item.PostUpdateTime = item.UpdatedAt + } else if !item.CreatedAt.IsZero() { + item.PostUpdateTime = item.CreatedAt + } + if _, err = x.Context(ctx).Update(item, &QuestionPostTime{ID: item.ID}); err != nil { + log.Errorf("update %+v config failed: %s", item, err) + return fmt.Errorf("update question failed: %w", err) + } + } + + } + + return nil +} diff --git a/internal/migrations/v13.go b/internal/migrations/v13.go new file mode 100644 index 000000000..30bfbb542 --- /dev/null +++ b/internal/migrations/v13.go @@ -0,0 +1,425 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + "time" + "xorm.io/builder" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/permission" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func updateCount(ctx context.Context, x *xorm.Engine) error { + fns := []func(ctx context.Context, x *xorm.Engine) error{ + inviteAnswer, + addPrivilegeForInviteSomeoneToAnswer, + addGravatarBaseURL, + updateQuestionCount, + updateTagCount, + updateUserQuestionCount, + updateUserAnswerCount, + inBoxData, + } + for _, fn := range fns { + if err := fn(ctx, x); err != nil { + return err + } + } + return nil +} + +func addGravatarBaseURL(ctx context.Context, x *xorm.Engine) error { + usersSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeUsers, + } + exist, err := x.Context(ctx).Get(usersSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + content := &schema.SiteUsersReq{} + _ = json.Unmarshal([]byte(usersSiteInfo.Content), content) + content.GravatarBaseURL = "https://www.gravatar.com/avatar/" + data, _ := json.Marshal(content) + usersSiteInfo.Content = string(data) + + _, err = x.Context(ctx).ID(usersSiteInfo.ID).Cols("content").Update(usersSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + return nil +} + +func addPrivilegeForInviteSomeoneToAnswer(ctx context.Context, x *xorm.Engine) error { + // add rank for invite to answer + powers := []*entity.Power{ + {ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"}, + } + for _, power := range powers { + exist, err := x.Context(ctx).Get(&entity.Power{PowerType: power.PowerType}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(power.ID).Update(power) + } else { + _, err = x.Context(ctx).Insert(power) + } + if err != nil { + return err + } + } + rolePowerRels := []*entity.RolePowerRel{ + {RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer}, + {RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer}, + } + for _, rel := range rolePowerRels { + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + if err != nil { + return err + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(rel) + if err != nil { + return err + } + } + + defaultConfigTable := []*entity.Config{ + {ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + return fmt.Errorf("add config failed: %w", err) + } + } + return nil +} + +func updateQuestionCount(ctx context.Context, x *xorm.Engine) error { + //question answer count + answers := make([]AnswerV13, 0) + err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) + if err != nil { + return fmt.Errorf("get answers failed: %w", err) + } + questionAnswerCount := make(map[string]int) + for _, answer := range answers { + _, ok := questionAnswerCount[answer.QuestionID] + if !ok { + questionAnswerCount[answer.QuestionID] = 1 + } else { + questionAnswerCount[answer.QuestionID]++ + } + } + questionList := make([]QuestionV13, 0) + err = x.Context(ctx).Find(&questionList, &QuestionV13{}) + if err != nil { + return fmt.Errorf("get questions failed: %w", err) + } + for _, item := range questionList { + _, ok := questionAnswerCount[item.ID] + if ok { + item.AnswerCount = questionAnswerCount[item.ID] + if _, err = x.Context(ctx).Cols("answer_count").Update(item, &QuestionV13{ID: item.ID}); err != nil { + log.Errorf("update %+v config failed: %s", item, err) + return fmt.Errorf("update question failed: %w", err) + } + } + } + + return nil +} + +// updateTagCount update tag count +func updateTagCount(ctx context.Context, x *xorm.Engine) error { + tagRelList := make([]entity.TagRel, 0) + err := x.Context(ctx).Find(&tagRelList, &entity.TagRel{}) + if err != nil { + return fmt.Errorf("get tag rel failed: %w", err) + } + questionIDs := make([]string, 0) + questionsAvailableMap := make(map[string]bool) + questionsHideMap := make(map[string]bool) + for _, item := range tagRelList { + questionIDs = append(questionIDs, item.ObjectID) + questionsAvailableMap[item.ObjectID] = false + questionsHideMap[item.ObjectID] = false + } + questionList := make([]QuestionV13, 0) + err = x.Context(ctx).In("id", questionIDs).And(builder.Lt{"question.status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) + if err != nil { + return fmt.Errorf("get questions failed: %w", err) + } + for _, question := range questionList { + _, ok := questionsAvailableMap[question.ID] + if ok { + questionsAvailableMap[question.ID] = true + if question.Show == entity.QuestionHide { + questionsHideMap[question.ID] = true + } + } + } + + for id, ok := range questionsHideMap { + if ok { + if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil { + log.Errorf("update %+v config failed: %s", id, err) + } + } + } + + for id, ok := range questionsAvailableMap { + if !ok { + if _, err = x.Context(ctx).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil { + log.Errorf("update %+v config failed: %s", id, err) + } + } + } + + //select tag count + newTagRelList := make([]entity.TagRel, 0) + err = x.Context(ctx).Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable}) + if err != nil { + return fmt.Errorf("get tag rel failed: %w", err) + } + tagCountMap := make(map[string]int) + for _, v := range newTagRelList { + _, ok := tagCountMap[v.TagID] + if !ok { + tagCountMap[v.TagID] = 1 + } else { + tagCountMap[v.TagID]++ + } + } + TagList := make([]entity.Tag, 0) + err = x.Context(ctx).Find(&TagList, &entity.Tag{}) + if err != nil { + return fmt.Errorf("get tag failed: %w", err) + } + for _, tag := range TagList { + _, ok := tagCountMap[tag.ID] + if ok { + tag.QuestionCount = tagCountMap[tag.ID] + if _, err = x.Context(ctx).Update(tag, &entity.Tag{ID: tag.ID}); err != nil { + log.Errorf("update %+v tag failed: %s", tag.ID, err) + return fmt.Errorf("update tag failed: %w", err) + } + } else { + tag.QuestionCount = 0 + if _, err = x.Context(ctx).Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil { + log.Errorf("update %+v tag failed: %s", tag.ID, err) + return fmt.Errorf("update tag failed: %w", err) + } + } + } + return nil +} + +// updateUserQuestionCount update user question count +func updateUserQuestionCount(ctx context.Context, x *xorm.Engine) error { + questionList := make([]QuestionV13, 0) + err := x.Context(ctx).Where(builder.Lt{"status": entity.QuestionStatusDeleted}).Find(&questionList, &QuestionV13{}) + if err != nil { + return fmt.Errorf("get question failed: %w", err) + } + userQuestionCountMap := make(map[string]int) + for _, question := range questionList { + _, ok := userQuestionCountMap[question.UserID] + if !ok { + userQuestionCountMap[question.UserID] = 1 + } else { + userQuestionCountMap[question.UserID]++ + } + } + userList := make([]entity.User, 0) + err = x.Context(ctx).Find(&userList, &entity.User{}) + if err != nil { + return fmt.Errorf("get user failed: %w", err) + } + for _, user := range userList { + _, ok := userQuestionCountMap[user.ID] + if ok { + user.QuestionCount = userQuestionCountMap[user.ID] + if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { + log.Errorf("update %+v user failed: %s", user.ID, err) + return fmt.Errorf("update user failed: %w", err) + } + } else { + user.QuestionCount = 0 + if _, err = x.Context(ctx).Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil { + log.Errorf("update %+v user failed: %s", user.ID, err) + return fmt.Errorf("update user failed: %w", err) + } + } + } + return nil +} + +type AnswerV13 struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + QuestionID string `xorm:"not null default 0 BIGINT(20) question_id"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + Status int `xorm:"not null default 1 INT(11) status"` + Accepted int `xorm:"not null default 1 INT(11) adopted"` +} + +func (AnswerV13) TableName() string { + return "answer" +} + +// updateUserAnswerCount update user answer count +func updateUserAnswerCount(ctx context.Context, x *xorm.Engine) error { + answers := make([]AnswerV13, 0) + err := x.Context(ctx).Find(&answers, &AnswerV13{Status: entity.AnswerStatusAvailable}) + if err != nil { + return fmt.Errorf("get answers failed: %w", err) + } + userAnswerCount := make(map[string]int) + for _, answer := range answers { + _, ok := userAnswerCount[answer.UserID] + if !ok { + userAnswerCount[answer.UserID] = 1 + } else { + userAnswerCount[answer.UserID]++ + } + } + userList := make([]entity.User, 0) + err = x.Context(ctx).Find(&userList, &entity.User{}) + if err != nil { + return fmt.Errorf("get user failed: %w", err) + } + for _, user := range userList { + _, ok := userAnswerCount[user.ID] + if ok { + user.AnswerCount = userAnswerCount[user.ID] + if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { + log.Errorf("update %+v user failed: %s", user.ID, err) + return fmt.Errorf("update user failed: %w", err) + } + } else { + user.AnswerCount = 0 + if _, err = x.Context(ctx).Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil { + log.Errorf("update %+v user failed: %s", user.ID, err) + return fmt.Errorf("update user failed: %w", err) + } + } + } + return nil +} + +type QuestionV13 struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + InviteUserID string `xorm:"TEXT invite_user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + Title string `xorm:"not null default '' VARCHAR(150) title"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` + ViewCount int `xorm:"not null default 0 INT(11) view_count"` + UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` + FollowCount int `xorm:"not null default 0 INT(11) follow_count"` + AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` + LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` +} + +func (QuestionV13) TableName() string { + return "question" +} + +func inviteAnswer(ctx context.Context, x *xorm.Engine) error { + err := x.Context(ctx).Sync(new(QuestionV13)) + if err != nil { + return err + } + return nil +} + +// inBoxData Classify messages +func inBoxData(ctx context.Context, x *xorm.Engine) error { + type Notification struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"` + Content string `xorm:"not null TEXT content"` + Type int `xorm:"not null default 0 INT(11) type"` + MsgType int `xorm:"not null default 0 INT(11) msg_type"` + IsRead int `xorm:"not null default 1 INT(11) is_read"` + Status int `xorm:"not null default 1 INT(11) status"` + } + err := x.Context(ctx).Sync(new(Notification)) + if err != nil { + return err + } + msglist := make([]entity.Notification, 0) + err = x.Context(ctx).Find(&msglist, &entity.Notification{}) + if err != nil { + return fmt.Errorf("get Notification failed: %w", err) + } + for _, v := range msglist { + Content := &schema.NotificationContent{} + err := json.Unmarshal([]byte(v.Content), Content) + if err != nil { + continue + } + _, ok := constant.NotificationMsgTypeMapping[Content.NotificationAction] + if ok { + v.MsgType = constant.NotificationMsgTypeMapping[Content.NotificationAction] + if _, err = x.Context(ctx).Update(v, &entity.Notification{ID: v.ID}); err != nil { + log.Errorf("update %+v Notification failed: %s", v.ID, err) + } + } + } + + return nil +} diff --git a/internal/migrations/v14.go b/internal/migrations/v14.go new file mode 100644 index 000000000..ee3a3d0d6 --- /dev/null +++ b/internal/migrations/v14.go @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "time" + "xorm.io/xorm/schemas" + + "xorm.io/xorm" +) + +func updateTheLengthOfRevisionContent(ctx context.Context, x *xorm.Engine) (err error) { + sess := x.Context(ctx) + if x.Dialect().URI().DBType == schemas.MYSQL { + _, err = sess.Exec("ALTER TABLE `revision` CHANGE `content` `content` MEDIUMTEXT NOT NULL;") + } + return err +} + +type RevisionV14 struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + ObjectType int `xorm:"not null default 0 INT(11) object_type"` + ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` + Title string `xorm:"not null default '' VARCHAR(255) title"` + Content string `xorm:"not null MEDIUMTEXT content"` + Log string `xorm:"VARCHAR(255) log"` + Status int `xorm:"not null default 1 INT(11) status"` + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` +} + +func (RevisionV14) TableName() string { + return "revision" +} diff --git a/internal/migrations/v15.go b/internal/migrations/v15.go new file mode 100644 index 000000000..5195c105a --- /dev/null +++ b/internal/migrations/v15.go @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addNoticeConfig(ctx context.Context, x *xorm.Engine) error { + return x.Context(ctx).Sync(new(entity.UserNotificationConfig)) +} diff --git a/internal/migrations/v16.go b/internal/migrations/v16.go new file mode 100644 index 000000000..11643600c --- /dev/null +++ b/internal/migrations/v16.go @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func setDefaultUserNotificationConfig(ctx context.Context, x *xorm.Engine) error { + userIDs := make([]string, 0) + err := x.Context(ctx).Table("user").Select("id").Find(&userIDs) + if err != nil { + return err + } + + for _, id := range userIDs { + bean := entity.UserNotificationConfig{UserID: id, Source: string(constant.InboxSource)} + exist, err := x.Context(ctx).Get(&bean) + if err != nil { + log.Error(err) + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(&entity.UserNotificationConfig{ + UserID: id, + Source: string(constant.InboxSource), + Channels: `[{"key":"email","enable":true}]`, + Enabled: true, + }) + if err != nil { + log.Error(err) + } + } + return nil +} diff --git a/internal/migrations/v17.go b/internal/migrations/v17.go new file mode 100644 index 000000000..655e547a2 --- /dev/null +++ b/internal/migrations/v17.go @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/permission" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addRecoverPermission(ctx context.Context, x *xorm.Engine) error { + powers := []*entity.Power{ + {ID: 39, Name: "recover answer", PowerType: permission.AnswerUnDelete, Description: "recover deleted answer"}, + {ID: 40, Name: "recover question", PowerType: permission.QuestionUnDelete, Description: "recover deleted question"}, + {ID: 41, Name: "recover tag", PowerType: permission.TagUnDelete, Description: "recover deleted tag"}, + } + for _, power := range powers { + exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(power.ID).Update(power) + } else { + _, err = x.Context(ctx).Insert(power) + } + if err != nil { + return err + } + } + + rolePowerRels := []*entity.RolePowerRel{ + {RoleID: 2, PowerType: permission.AnswerUnDelete}, + {RoleID: 2, PowerType: permission.QuestionUnDelete}, + {RoleID: 2, PowerType: permission.TagUnDelete}, + + {RoleID: 3, PowerType: permission.AnswerUnDelete}, + {RoleID: 3, PowerType: permission.QuestionUnDelete}, + {RoleID: 3, PowerType: permission.TagUnDelete}, + } + for _, rel := range rolePowerRels { + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + if err != nil { + return err + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(rel) + if err != nil { + return err + } + } + + defaultConfigTable := []*entity.Config{ + {ID: 128, Key: "rank.answer.undeleted", Value: `-1`}, + {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, + {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + return nil +} diff --git a/internal/migrations/v18.go b/internal/migrations/v18.go new file mode 100644 index 000000000..89db524f5 --- /dev/null +++ b/internal/migrations/v18.go @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "xorm.io/xorm" +) + +func addPasswordLoginControl(ctx context.Context, x *xorm.Engine) error { + loginSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeLogin, + } + exist, err := x.Context(ctx).Get(loginSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + content := &schema.SiteLoginReq{} + _ = json.Unmarshal([]byte(loginSiteInfo.Content), content) + content.AllowPasswordLogin = true + data, _ := json.Marshal(content) + loginSiteInfo.Content = string(data) + _, err = x.Context(ctx).ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + + writeSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeWrite, + } + exist, err = x.Context(ctx).Get(writeSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + type OldSiteWriteReq struct { + RestrictAnswer bool `validate:"omitempty" form:"restrict_answer" json:"restrict_answer"` + RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"` + RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"` + ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"` + UserID string `json:"-"` + } + content := &OldSiteWriteReq{} + _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) + content.RestrictAnswer = true + data, _ := json.Marshal(content) + writeSiteInfo.Content = string(data) + _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + + type User struct { + Avatar string `xorm:"not null default '' VARCHAR(1024) avatar"` + } + return x.Context(ctx).Sync(new(User)) +} diff --git a/internal/migrations/v19.go b/internal/migrations/v19.go new file mode 100644 index 000000000..b45d374bb --- /dev/null +++ b/internal/migrations/v19.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addNotificationPluginAndThemeConfig(ctx context.Context, x *xorm.Engine) error { + type User struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + ColorScheme string `xorm:"not null default '' VARCHAR(100) color_scheme"` + } + return x.Context(ctx).Sync(new(entity.PluginUserConfig), new(User)) +} diff --git a/internal/migrations/v2.go b/internal/migrations/v2.go new file mode 100644 index 000000000..4e17597dc --- /dev/null +++ b/internal/migrations/v2.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "xorm.io/xorm" +) + +func addTagRecommendedAndReserved(ctx context.Context, x *xorm.Engine) error { + type Tag struct { + ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` + SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` + Recommend bool `xorm:"not null default false BOOL recommend"` + Reserved bool `xorm:"not null default false BOOL reserved"` + } + return x.Context(ctx).Sync(new(Tag)) +} diff --git a/internal/migrations/v20.go b/internal/migrations/v20.go new file mode 100644 index 000000000..74242aaf4 --- /dev/null +++ b/internal/migrations/v20.go @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addReview(ctx context.Context, x *xorm.Engine) error { + c := &entity.Config{Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only."}`} + if _, err := x.Context(ctx).Update(c, &entity.Config{Key: "reason.not_clarity"}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + return x.Context(ctx).Sync(new(entity.Review)) +} diff --git a/internal/migrations/v21.go b/internal/migrations/v21.go new file mode 100644 index 000000000..880852f8e --- /dev/null +++ b/internal/migrations/v21.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "xorm.io/xorm" +) + +func addQuestionHotScore(ctx context.Context, x *xorm.Engine) error { + type Question struct { + HotScore int `xorm:"not null default 0 INT(11) hot_score"` + } + return x.Context(ctx).Sync(new(Question)) +} diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go new file mode 100644 index 000000000..14292a1e7 --- /dev/null +++ b/internal/migrations/v22.go @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/unique" + "xorm.io/xorm" +) + +func addBadges(ctx context.Context, x *xorm.Engine) (err error) { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x}) + + err = x.Context(ctx).Sync(new(entity.Badge), new(entity.BadgeGroup), new(entity.BadgeAward)) + if err != nil { + return fmt.Errorf("sync table failed: %w", err) + } + + for _, badgeGroup := range defaultBadgeGroupTable { + exist, err := x.Context(ctx).Get(&entity.BadgeGroup{ID: badgeGroup.ID}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(badgeGroup.ID).Update(badgeGroup) + } else { + _, err = x.Context(ctx).Insert(badgeGroup) + } + if err != nil { + return fmt.Errorf("insert badge group failed: %w", err) + } + } + + for _, badge := range defaultBadgeTable { + beans := &entity.Badge{Name: badge.Name} + exist, err := x.Context(ctx).Get(beans) + if err != nil { + return err + } + if exist { + badge.ID = beans.ID + _, err = x.Context(ctx).ID(beans.ID).Update(badge) + continue + } + badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) + if err != nil { + return err + } + + if _, err := x.Context(ctx).Insert(badge); err != nil { + return err + } + } + return +} diff --git a/internal/migrations/v23.go b/internal/migrations/v23.go new file mode 100644 index 000000000..49a243765 --- /dev/null +++ b/internal/migrations/v23.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addQuestionLink(ctx context.Context, x *xorm.Engine) (err error) { + return x.Context(ctx).Sync(new(entity.QuestionLink)) +} diff --git a/internal/migrations/v24.go b/internal/migrations/v24.go new file mode 100644 index 000000000..19358af45 --- /dev/null +++ b/internal/migrations/v24.go @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + + "xorm.io/xorm" +) + +func addQuestionLinkedCount(ctx context.Context, x *xorm.Engine) error { + writeSiteInfo := &entity.SiteInfo{ + Type: constant.SiteTypeWrite, + } + exist, err := x.Context(ctx).Get(writeSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + type OldSiteWriteReq struct { + RestrictAnswer bool `json:"restrict_answer"` + RequiredTag bool `json:"required_tag"` + RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"` + ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"` + MaxImageSize int `json:"max_image_size"` + MaxAttachmentSize int `json:"max_attachment_size"` + MaxImageMegapixel int `json:"max_image_megapixel"` + AuthorizedImageExtensions []string `json:"authorized_image_extensions"` + AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"` + } + content := &OldSiteWriteReq{} + _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) + content.MaxImageSize = 4 + content.MaxAttachmentSize = 8 + content.MaxImageMegapixel = 40 + content.AuthorizedImageExtensions = []string{"jpg", "jpeg", "png", "gif", "webp"} + content.AuthorizedAttachmentExtensions = []string{} + data, _ := json.Marshal(content) + writeSiteInfo.Content = string(data) + _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) + if err != nil { + return fmt.Errorf("update site info failed: %w", err) + } + } + + type Question struct { + LinkedCount int `xorm:"not null default 0 INT(11) linked_count"` + } + return x.Context(ctx).Sync(new(entity.Question)) +} diff --git a/internal/migrations/v25.go b/internal/migrations/v25.go new file mode 100644 index 000000000..560a852ac --- /dev/null +++ b/internal/migrations/v25.go @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addFileRecord(ctx context.Context, x *xorm.Engine) error { + if err := x.Context(ctx).Sync(new(entity.FileRecord)); err != nil { + return err + } + + // Set default external_content_display to always_display + legalInfo := &entity.SiteInfo{Type: "legal"} + exist, err := x.Context(ctx).Get(legalInfo) + if err != nil { + return fmt.Errorf("get legal config failed: %w", err) + } + legalConfig := make(map[string]interface{}) + if exist { + if err := json.Unmarshal([]byte(legalInfo.Content), &legalConfig); err != nil { + return fmt.Errorf("unmarshal legal config failed: %w", err) + } + } + legalConfig["external_content_display"] = "always_display" + legalConfigBytes, _ := json.Marshal(legalConfig) + if exist { + legalInfo.Content = string(legalConfigBytes) + _, err = x.Context(ctx).ID(legalInfo.ID).Cols("content").Update(legalInfo) + if err != nil { + return fmt.Errorf("update legal config failed: %w", err) + } + } else { + legalInfo.Content = string(legalConfigBytes) + legalInfo.Status = 1 + _, err = x.Context(ctx).Insert(legalInfo) + if err != nil { + return fmt.Errorf("insert legal config failed: %w", err) + } + } + return nil +} diff --git a/internal/migrations/v26.go b/internal/migrations/v26.go new file mode 100644 index 000000000..008a094a4 --- /dev/null +++ b/internal/migrations/v26.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addPluginKVStorage(ctx context.Context, x *xorm.Engine) error { + return x.Context(ctx).Sync(new(entity.PluginKVStorage)) +} diff --git a/internal/migrations/v27.go b/internal/migrations/v27.go new file mode 100644 index 000000000..a0c4ef8a1 --- /dev/null +++ b/internal/migrations/v27.go @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "xorm.io/xorm" +) + +func addSuspendedUntilToUser(ctx context.Context, x *xorm.Engine) error { + type User struct { + SuspendedUntil *time.Time `xorm:"DATETIME suspended_until"` + } + return x.Context(ctx).Sync(new(User)) +} + +func moveUserConfigToInterface(ctx context.Context, x *xorm.Engine) error { + if err := addSuspendedUntilToUser(ctx, x); err != nil { + return fmt.Errorf("add suspended_until to user failed: %w", err) + } + + // Get old interface config + interfaceSiteInfo := &entity.SiteInfo{Type: constant.SiteTypeInterface} + exist, err := x.Context(ctx).Get(interfaceSiteInfo) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + return fmt.Errorf("interface site info not found") + } + + interfaceConfig := &schema.SiteInterfaceReq{} + _ = json.Unmarshal([]byte(interfaceSiteInfo.Content), interfaceConfig) + + // Get old user config + usersConfig := &entity.SiteInfo{Type: constant.SiteTypeUsers} + exist, err = x.Context(ctx).Get(usersConfig) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + return fmt.Errorf("users site info not found") + } + + siteUsers := &schema.SiteUsersReq{} + _ = json.Unmarshal([]byte(usersConfig.Content), siteUsers) + + interfaceConfig.DefaultAvatar = siteUsers.DefaultAvatar + interfaceConfig.GravatarBaseURL = siteUsers.GravatarBaseURL + + interfaceConfigByte, _ := json.Marshal(interfaceConfig) + interfaceSiteInfo.Content = string(interfaceConfigByte) + + _, err = x.Context(ctx).ID(interfaceSiteInfo.ID).Update(interfaceSiteInfo) + if err != nil { + return fmt.Errorf("insert site info failed: %w", err) + } + return nil +} diff --git a/internal/migrations/v3.go b/internal/migrations/v3.go new file mode 100644 index 000000000..a191d7258 --- /dev/null +++ b/internal/migrations/v3.go @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func addActivityTimeline(ctx context.Context, x *xorm.Engine) (err error) { + switch x.Dialect().URI().DBType { + case schemas.MYSQL: + _, err = x.Context(ctx).Exec("ALTER TABLE `answer` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") + if err != nil { + return err + } + _, err = x.Context(ctx).Exec("ALTER TABLE `question` CHANGE `updated_at` `updated_at` TIMESTAMP NULL DEFAULT NULL") + if err != nil { + return err + } + case schemas.POSTGRES: + _, err = x.Context(ctx).Exec(`ALTER TABLE "answer" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) + if err != nil { + return err + } + _, err = x.Context(ctx).Exec(`ALTER TABLE "question" ALTER COLUMN "updated_at" DROP NOT NULL, ALTER COLUMN "updated_at" SET DEFAULT NULL`) + if err != nil { + return err + } + case schemas.SQLITE: + _, err = x.Context(ctx).Exec(`DROP INDEX "IDX_answer_user_id"; + +ALTER TABLE "answer" RENAME TO "_answer_old_v3"; + +CREATE TABLE "answer" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME DEFAULT NULL, + "question_id" INTEGER NOT NULL DEFAULT 0, + "user_id" INTEGER NOT NULL DEFAULT 0, + "original_text" TEXT NOT NULL, + "parsed_text" TEXT NOT NULL, + "status" INTEGER NOT NULL DEFAULT 1, + "adopted" INTEGER NOT NULL DEFAULT 1, + "comment_count" INTEGER NOT NULL DEFAULT 0, + "vote_count" INTEGER NOT NULL DEFAULT 0, + "revision_id" INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO "answer" ("id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id") SELECT "id", "created_at", "updated_at", "question_id", "user_id", "original_text", "parsed_text", "status", "adopted", "comment_count", "vote_count", "revision_id" FROM "_answer_old_v3"; + +CREATE INDEX "IDX_answer_user_id" +ON "answer" ( + "user_id" ASC +); +DROP INDEX "IDX_question_user_id"; + +ALTER TABLE "question" RENAME TO "_question_old_v3"; + +CREATE TABLE "question" ( + "id" INTEGER NOT NULL, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" DATETIME DEFAULT NULL, + "user_id" INTEGER NOT NULL DEFAULT 0, + "title" TEXT NOT NULL DEFAULT '', + "original_text" TEXT NOT NULL, + "parsed_text" TEXT NOT NULL, + "status" INTEGER NOT NULL DEFAULT 1, + "view_count" INTEGER NOT NULL DEFAULT 0, + "unique_view_count" INTEGER NOT NULL DEFAULT 0, + "vote_count" INTEGER NOT NULL DEFAULT 0, + "answer_count" INTEGER NOT NULL DEFAULT 0, + "collection_count" INTEGER NOT NULL DEFAULT 0, + "follow_count" INTEGER NOT NULL DEFAULT 0, + "accepted_answer_id" INTEGER NOT NULL DEFAULT 0, + "last_answer_id" INTEGER NOT NULL DEFAULT 0, + "post_update_time" DATETIME DEFAULT CURRENT_TIMESTAMP, + "revision_id" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +); + +INSERT INTO "question" ("id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id") SELECT "id", "created_at", "updated_at", "user_id", "title", "original_text", "parsed_text", "status", "view_count", "unique_view_count", "vote_count", "answer_count", "collection_count", "follow_count", "accepted_answer_id", "last_answer_id", "post_update_time", "revision_id" FROM "_question_old_v3"; + +CREATE INDEX "IDX_question_user_id" +ON "question" ( + "user_id" ASC +);`) + if err != nil { + return err + } + } + + // only increasing field length to 128 + type Config struct { + ID int `xorm:"not null pk autoincr INT(11) id"` + Key string `xorm:"unique VARCHAR(128) key"` + } + if err := x.Context(ctx).Sync(new(Config)); err != nil { + return fmt.Errorf("sync config table failed: %w", err) + } + defaultConfigTable := []*entity.Config{ + {ID: 36, Key: "rank.question.add", Value: `1`}, + {ID: 37, Key: "rank.question.edit", Value: `200`}, + {ID: 38, Key: "rank.question.delete", Value: `-1`}, + {ID: 39, Key: "rank.question.vote_up", Value: `15`}, + {ID: 40, Key: "rank.question.vote_down", Value: `125`}, + {ID: 41, Key: "rank.answer.add", Value: `1`}, + {ID: 42, Key: "rank.answer.edit", Value: `200`}, + {ID: 43, Key: "rank.answer.delete", Value: `-1`}, + {ID: 44, Key: "rank.answer.accept", Value: `-1`}, + {ID: 45, Key: "rank.answer.vote_up", Value: `15`}, + {ID: 46, Key: "rank.answer.vote_down", Value: `125`}, + {ID: 47, Key: "rank.comment.add", Value: `1`}, + {ID: 48, Key: "rank.comment.edit", Value: `-1`}, + {ID: 49, Key: "rank.comment.delete", Value: `-1`}, + {ID: 50, Key: "rank.report.add", Value: `1`}, + {ID: 51, Key: "rank.tag.add", Value: `1500`}, + {ID: 52, Key: "rank.tag.edit", Value: `100`}, + {ID: 53, Key: "rank.tag.delete", Value: `-1`}, + {ID: 54, Key: "rank.tag.synonym", Value: `20000`}, + {ID: 55, Key: "rank.link.url_limit", Value: `10`}, + {ID: 56, Key: "rank.vote.detail", Value: `0`}, + + {ID: 87, Key: "question.asked", Value: `0`}, + {ID: 88, Key: "question.closed", Value: `0`}, + {ID: 89, Key: "question.reopened", Value: `0`}, + {ID: 90, Key: "question.answered", Value: `0`}, + {ID: 91, Key: "question.commented", Value: `0`}, + {ID: 92, Key: "question.accept", Value: `0`}, + {ID: 93, Key: "question.edited", Value: `0`}, + {ID: 94, Key: "question.rollback", Value: `0`}, + {ID: 95, Key: "question.deleted", Value: `0`}, + {ID: 96, Key: "question.undeleted", Value: `0`}, + {ID: 97, Key: "answer.answered", Value: `0`}, + {ID: 98, Key: "answer.commented", Value: `0`}, + {ID: 99, Key: "answer.edited", Value: `0`}, + {ID: 100, Key: "answer.rollback", Value: `0`}, + {ID: 101, Key: "answer.undeleted", Value: `0`}, + {ID: 102, Key: "tag.created", Value: `0`}, + {ID: 103, Key: "tag.edited", Value: `0`}, + {ID: 104, Key: "tag.rollback", Value: `0`}, + {ID: 105, Key: "tag.deleted", Value: `0`}, + {ID: 106, Key: "tag.undeleted", Value: `0`}, + + {ID: 107, Key: "rank.comment.vote_up", Value: `1`}, + {ID: 108, Key: "rank.comment.vote_down", Value: `1`}, + {ID: 109, Key: "rank.question.edit_without_review", Value: `2000`}, + {ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`}, + {ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`}, + {ID: 112, Key: "rank.answer.audit", Value: `2000`}, + {ID: 113, Key: "rank.question.audit", Value: `2000`}, + {ID: 114, Key: "rank.tag.audit", Value: `20000`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + + type Revision struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"` + ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"` + } + type Activity struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"` + UserID string `xorm:"not null index BIGINT(20) user_id"` + TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"` + ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"` + RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"` + OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"` + } + type Tag struct { + ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"` + SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"` + UserID string `xorm:"not null default 0 BIGINT(20) user_id"` + } + type Question struct { + ID string `xorm:"not null pk BIGINT(20) id"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + } + type Answer struct { + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + } + + err = x.Context(ctx).Sync(new(Activity), new(Revision), new(Tag), new(Question), new(Answer)) + if err != nil { + return fmt.Errorf("sync table failed %w", err) + } + return nil +} diff --git a/internal/migrations/v4.go b/internal/migrations/v4.go new file mode 100644 index 000000000..d4079a143 --- /dev/null +++ b/internal/migrations/v4.go @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/permission" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addRoleFeatures(ctx context.Context, x *xorm.Engine) error { + err := x.Context(ctx).Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel)) + if err != nil { + return err + } + + roles := []*entity.Role{ + {ID: 1, Name: "User", Description: "Default with no special access."}, + {ID: 2, Name: "Admin", Description: "Have the full power to access the site."}, + {ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."}, + } + + // insert default roles + for _, role := range roles { + exist, err := x.Context(ctx).Get(&entity.Role{ID: role.ID, Name: role.Name}) + if err != nil { + return err + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(role) + if err != nil { + return err + } + } + + powers := []*entity.Power{ + {ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"}, + {ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"}, + {ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"}, + {ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"}, + {ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"}, + {ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"}, + {ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"}, + {ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"}, + {ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"}, + {ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"}, + {ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"}, + {ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"}, + {ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"}, + {ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"}, + {ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"}, + {ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"}, + {ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"}, + {ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"}, + {ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"}, + {ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"}, + {ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"}, + {ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"}, + {ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"}, + {ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"}, + {ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"}, + {ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"}, + {ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"}, + {ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"}, + {ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"}, + {ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"}, + {ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"}, + {ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"}, + {ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"}, + } + // insert default powers + for _, power := range powers { + exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(power.ID).Update(power) + } else { + _, err = x.Context(ctx).Insert(power) + } + if err != nil { + return err + } + } + + rolePowerRels := []*entity.RolePowerRel{ + {RoleID: 2, PowerType: permission.AdminAccess}, + {RoleID: 2, PowerType: permission.QuestionAdd}, + {RoleID: 2, PowerType: permission.QuestionEdit}, + {RoleID: 2, PowerType: permission.QuestionEditWithoutReview}, + {RoleID: 2, PowerType: permission.QuestionDelete}, + {RoleID: 2, PowerType: permission.QuestionClose}, + {RoleID: 2, PowerType: permission.QuestionReopen}, + {RoleID: 2, PowerType: permission.QuestionVoteUp}, + {RoleID: 2, PowerType: permission.QuestionVoteDown}, + {RoleID: 2, PowerType: permission.AnswerAdd}, + {RoleID: 2, PowerType: permission.AnswerEdit}, + {RoleID: 2, PowerType: permission.AnswerEditWithoutReview}, + {RoleID: 2, PowerType: permission.AnswerDelete}, + {RoleID: 2, PowerType: permission.AnswerAccept}, + {RoleID: 2, PowerType: permission.AnswerVoteUp}, + {RoleID: 2, PowerType: permission.AnswerVoteDown}, + {RoleID: 2, PowerType: permission.CommentAdd}, + {RoleID: 2, PowerType: permission.CommentEdit}, + {RoleID: 2, PowerType: permission.CommentDelete}, + {RoleID: 2, PowerType: permission.CommentVoteUp}, + {RoleID: 2, PowerType: permission.CommentVoteDown}, + {RoleID: 2, PowerType: permission.ReportAdd}, + {RoleID: 2, PowerType: permission.TagAdd}, + {RoleID: 2, PowerType: permission.TagEdit}, + {RoleID: 2, PowerType: permission.TagEditSlugName}, + {RoleID: 2, PowerType: permission.TagEditWithoutReview}, + {RoleID: 2, PowerType: permission.TagDelete}, + {RoleID: 2, PowerType: permission.TagSynonym}, + {RoleID: 2, PowerType: permission.LinkUrlLimit}, + {RoleID: 2, PowerType: permission.VoteDetail}, + {RoleID: 2, PowerType: permission.AnswerAudit}, + {RoleID: 2, PowerType: permission.QuestionAudit}, + {RoleID: 2, PowerType: permission.TagAudit}, + {RoleID: 2, PowerType: permission.TagUseReservedTag}, + + {RoleID: 3, PowerType: permission.QuestionAdd}, + {RoleID: 3, PowerType: permission.QuestionEdit}, + {RoleID: 3, PowerType: permission.QuestionEditWithoutReview}, + {RoleID: 3, PowerType: permission.QuestionDelete}, + {RoleID: 3, PowerType: permission.QuestionClose}, + {RoleID: 3, PowerType: permission.QuestionReopen}, + {RoleID: 3, PowerType: permission.QuestionVoteUp}, + {RoleID: 3, PowerType: permission.QuestionVoteDown}, + {RoleID: 3, PowerType: permission.AnswerAdd}, + {RoleID: 3, PowerType: permission.AnswerEdit}, + {RoleID: 3, PowerType: permission.AnswerEditWithoutReview}, + {RoleID: 3, PowerType: permission.AnswerDelete}, + {RoleID: 3, PowerType: permission.AnswerAccept}, + {RoleID: 3, PowerType: permission.AnswerVoteUp}, + {RoleID: 3, PowerType: permission.AnswerVoteDown}, + {RoleID: 3, PowerType: permission.CommentAdd}, + {RoleID: 3, PowerType: permission.CommentEdit}, + {RoleID: 3, PowerType: permission.CommentDelete}, + {RoleID: 3, PowerType: permission.CommentVoteUp}, + {RoleID: 3, PowerType: permission.CommentVoteDown}, + {RoleID: 3, PowerType: permission.ReportAdd}, + {RoleID: 3, PowerType: permission.TagAdd}, + {RoleID: 3, PowerType: permission.TagEdit}, + {RoleID: 3, PowerType: permission.TagEditSlugName}, + {RoleID: 3, PowerType: permission.TagEditWithoutReview}, + {RoleID: 3, PowerType: permission.TagDelete}, + {RoleID: 3, PowerType: permission.TagSynonym}, + {RoleID: 3, PowerType: permission.LinkUrlLimit}, + {RoleID: 3, PowerType: permission.VoteDetail}, + {RoleID: 3, PowerType: permission.AnswerAudit}, + {RoleID: 3, PowerType: permission.QuestionAudit}, + {RoleID: 3, PowerType: permission.TagAudit}, + {RoleID: 3, PowerType: permission.TagUseReservedTag}, + } + + // insert default powers + for _, rel := range rolePowerRels { + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + if err != nil { + return err + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(rel) + if err != nil { + return err + } + } + + adminUserRoleRel := &entity.UserRoleRel{ + UserID: "1", + RoleID: 2, + } + + exist, err := x.Context(ctx).Get(adminUserRoleRel) + if err != nil { + return err + } + if !exist { + _, err = x.Context(ctx).Insert(adminUserRoleRel) + if err != nil { + return err + } + } + + defaultConfigTable := []*entity.Config{ + {ID: 115, Key: "rank.question.close", Value: `-1`}, + {ID: 116, Key: "rank.question.reopen", Value: `-1`}, + {ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + return nil +} diff --git a/internal/migrations/v5.go b/internal/migrations/v5.go new file mode 100644 index 000000000..91d12f159 --- /dev/null +++ b/internal/migrations/v5.go @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addThemeAndPrivateMode(ctx context.Context, x *xorm.Engine) error { + loginConfig := map[string]bool{ + "allow_new_registrations": true, + "login_required": false, + } + loginConfigDataBytes, _ := json.Marshal(loginConfig) + siteInfo := &entity.SiteInfo{ + Type: "login", + Content: string(loginConfigDataBytes), + Status: 1, + } + exist, err := x.Context(ctx).Get(&entity.SiteInfo{Type: siteInfo.Type}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + _, err = x.Context(ctx).Insert(siteInfo) + if err != nil { + return fmt.Errorf("insert site info failed: %w", err) + } + } + + themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"#0033ff","primary_color":"#0033ff"}}}` + themeSiteInfo := &entity.SiteInfo{ + Type: "theme", + Content: themeConfig, + Status: 1, + } + exist, err = x.Context(ctx).Get(&entity.SiteInfo{Type: themeSiteInfo.Type}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + _, err = x.Context(ctx).Insert(themeSiteInfo) + } + return err +} diff --git a/internal/migrations/v6.go b/internal/migrations/v6.go new file mode 100644 index 000000000..9171ad47a --- /dev/null +++ b/internal/migrations/v6.go @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apache/answer/internal/entity" + "xorm.io/xorm" +) + +func addNewAnswerNotification(ctx context.Context, x *xorm.Engine) error { + cond := &entity.Config{Key: "email.config"} + exists, err := x.Context(ctx).Get(cond) + if err != nil { + return fmt.Errorf("get email config failed: %w", err) + } + if !exists { + // This should be impossible except that the config was deleted manually by user. + _, err = x.Context(ctx).Insert(&entity.Config{ + Key: "email.config", + Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe"}`, + }) + if err != nil { + return fmt.Errorf("add email config failed: %v", err) + } + } + + m := make(map[string]interface{}) + _ = json.Unmarshal([]byte(cond.Value), &m) + m["new_answer_title"] = "[{{.SiteName}}] {{.DisplayName}} answered your question" + m["new_answer_body"] = "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" + m["new_comment_title"] = "[{{.SiteName}}] {{.DisplayName}} commented on your post" + m["new_comment_body"] = "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\nView it on {{.SiteName}}

\n\nYou are receiving this because you authored the thread. Unsubscribe" + + val, _ := json.Marshal(m) + _, err = x.Context(ctx).ID(cond.ID).Update(&entity.Config{Value: string(val)}) + if err != nil { + return fmt.Errorf("update email config failed: %v", err) + } + return nil +} diff --git a/internal/migrations/v7.go b/internal/migrations/v7.go new file mode 100644 index 000000000..f4b64f64e --- /dev/null +++ b/internal/migrations/v7.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addPlugin(ctx context.Context, x *xorm.Engine) error { + defaultConfigTable := []*entity.Config{ + {ID: 118, Key: "plugin.status", Value: `{}`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID, Key: c.Key}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + + return x.Context(ctx).Sync(new(entity.PluginConfig), new(entity.UserExternalLogin)) +} diff --git a/internal/migrations/v8.go b/internal/migrations/v8.go new file mode 100644 index 000000000..cafb538e2 --- /dev/null +++ b/internal/migrations/v8.go @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + "time" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/permission" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func addRolePinAndHideFeatures(ctx context.Context, x *xorm.Engine) error { + powers := []*entity.Power{ + {ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"}, + {ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"}, + {ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"}, + {ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"}, + } + // insert default powers + for _, power := range powers { + exist, err := x.Context(ctx).Get(&entity.Power{ID: power.ID}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(power.ID).Update(power) + } else { + _, err = x.Context(ctx).Insert(power) + } + if err != nil { + return err + } + } + + rolePowerRels := []*entity.RolePowerRel{ + + {RoleID: 2, PowerType: permission.QuestionPin}, + {RoleID: 2, PowerType: permission.QuestionHide}, + {RoleID: 2, PowerType: permission.QuestionUnPin}, + {RoleID: 2, PowerType: permission.QuestionShow}, + + {RoleID: 3, PowerType: permission.QuestionPin}, + {RoleID: 3, PowerType: permission.QuestionHide}, + {RoleID: 3, PowerType: permission.QuestionUnPin}, + {RoleID: 3, PowerType: permission.QuestionShow}, + } + + // insert default powers + for _, rel := range rolePowerRels { + exist, err := x.Context(ctx).Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType}) + if err != nil { + return err + } + if exist { + continue + } + _, err = x.Context(ctx).Insert(rel) + if err != nil { + return err + } + } + + defaultConfigTable := []*entity.Config{ + {ID: 119, Key: "question.pin", Value: `0`}, + {ID: 120, Key: "question.unpin", Value: `0`}, + {ID: 121, Key: "question.show", Value: `0`}, + {ID: 122, Key: "question.hide", Value: `0`}, + {ID: 123, Key: "rank.question.pin", Value: `-1`}, + {ID: 124, Key: "rank.question.unpin", Value: `-1`}, + {ID: 125, Key: "rank.question.show", Value: `-1`}, + {ID: 126, Key: "rank.question.hide", Value: `-1`}, + } + for _, c := range defaultConfigTable { + exist, err := x.Context(ctx).Get(&entity.Config{ID: c.ID}) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if exist { + if _, err = x.Context(ctx).Update(c, &entity.Config{ID: c.ID}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + continue + } + if _, err = x.Context(ctx).Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil { + log.Errorf("insert %+v config failed: %s", c, err) + return fmt.Errorf("add config failed: %w", err) + } + } + + type Question struct { + ID string `xorm:"not null pk BIGINT(20) id"` + CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"` + UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"` + LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"` + Title string `xorm:"not null default '' VARCHAR(150) title"` + OriginalText string `xorm:"not null MEDIUMTEXT original_text"` + ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"` + Status int `xorm:"not null default 1 INT(11) status"` + Pin int `xorm:"not null default 1 INT(11) pin"` + Show int `xorm:"not null default 1 INT(11) show"` + ViewCount int `xorm:"not null default 0 INT(11) view_count"` + UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"` + VoteCount int `xorm:"not null default 0 INT(11) vote_count"` + AnswerCount int `xorm:"not null default 0 INT(11) answer_count"` + CollectionCount int `xorm:"not null default 0 INT(11) collection_count"` + FollowCount int `xorm:"not null default 0 INT(11) follow_count"` + AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"` + LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"` + PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"` + RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"` + } + err := x.Context(ctx).Sync(new(Question)) + if err != nil { + return err + } + + return nil +} diff --git a/internal/migrations/v9.go b/internal/migrations/v9.go new file mode 100644 index 000000000..5f20b74e5 --- /dev/null +++ b/internal/migrations/v9.go @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" +) + +func updateAcceptAnswerRank(ctx context.Context, x *xorm.Engine) error { + c := &entity.Config{ID: 44, Key: "rank.answer.accept", Value: `-1`} + if _, err := x.Context(ctx).Update(c, &entity.Config{ID: 44, Key: "rank.answer.accept"}); err != nil { + log.Errorf("update %+v config failed: %s", c, err) + return fmt.Errorf("update config failed: %w", err) + } + return nil +} diff --git a/internal/repo/activity/activity_repo.go b/internal/repo/activity/activity_repo.go new file mode 100644 index 000000000..1098a73ed --- /dev/null +++ b/internal/repo/activity/activity_repo.go @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_type" + "github.com/apache/answer/internal/service/config" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// activityRepo activity repository +type activityRepo struct { + data *data.Data + configService *config.ConfigService +} + +// NewActivityRepo new repository +func NewActivityRepo( + data *data.Data, + configService *config.ConfigService, +) activity.ActivityRepo { + return &activityRepo{ + data: data, + configService: configService, + } +} + +func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) ( + activityList []*entity.Activity, err error) { + activityList = make([]*entity.Activity, 0) + session := ar.data.DB.Context(ctx).Desc("id") + + if !showVote { + activityTypeNotShown := ar.getAllActivityType(ctx) + session.NotIn("activity_type", activityTypeNotShown) + } + err = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return activityList, nil +} + +func (ar *activityRepo) getAllActivityType(ctx context.Context) (activityTypes []int) { + var activityTypeNotShown []int + for _, key := range activity_type.VoteActivityTypeList { + id, err := ar.configService.GetIDByKey(ctx, key) + if err != nil { + log.Errorf("get config id by key [%s] error: %v", key, err) + } else { + activityTypeNotShown = append(activityTypeNotShown, id) + } + } + return activityTypeNotShown +} diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index 74ab0db60..db6e3bde4 100644 --- a/internal/repo/activity/answer_repo.go +++ b/internal/repo/activity/answer_repo.go @@ -1,339 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity import ( "context" + "fmt" + "github.com/segmentfault/pacman/log" + "time" + "xorm.io/builder" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/notice_queue" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) // AnswerActivityRepo answer accepted type AnswerActivityRepo struct { - data *data.Data - activityRepo activity_common.ActivityRepo - userRankRepo rank.UserRankRepo + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + notificationQueueService notice_queue.NotificationQueueService } -const ( - acceptAction = "accept" - acceptedAction = "accepted" - acceptCancelAction = "accept_cancel" - acceptedCancelAction = "accepted_cancel" -) - -var ( - acceptActionList = []string{acceptAction, acceptedAction} - acceptCancelActionList = []string{acceptCancelAction, acceptedCancelAction} -) - // NewAnswerActivityRepo new repository func NewAnswerActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, + notificationQueueService notice_queue.NotificationQueueService, ) activity.AnswerActivityRepo { return &AnswerActivityRepo{ - - data: data, - activityRepo: activityRepo, - userRankRepo: userRankRepo, + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + notificationQueueService: notificationQueueService, } } -// NewQuestionActivityRepo new repository -func NewQuestionActivityRepo( - data *data.Data, - activityRepo activity_common.ActivityRepo, - userRankRepo rank.UserRankRepo, -) activity.QuestionActivityRepo { - return &AnswerActivityRepo{ - data: data, - activityRepo: activityRepo, - userRankRepo: userRankRepo, - } -} +func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + err error) { + // save activity + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + userInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs()) + if err != nil { + return nil, err + } -func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) { - questionInfo := &entity.Question{} - exist, err := ar.data.DB.Where("id = ?", questionID).Get(questionInfo) + err = ar.saveActivitiesAvailable(session, op) + if err != nil { + return nil, err + } + + err = ar.changeUserRank(ctx, session, op, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil + }) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !exist { - return nil - } - // get all this object activity - activityList := make([]*entity.Activity, 0) - session := ar.data.DB.Where("has_rank = 1") - session.Where("cancelled = ?", entity.ActivityAvailable) - err = session.Find(&activityList, &entity.Activity{ObjectID: questionID}) + // notification + ar.sendAcceptAnswerNotification(ctx, op) + return nil +} + +func (ar *AnswerActivityRepo) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + err error) { + // pre check + activities, err := ar.getExistActivity(ctx, op) if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return err } - if len(activityList) == 0 { + var userIDs []string + for _, act := range activities { + if act.Cancelled == entity.ActivityCancelled { + continue + } + userIDs = append(userIDs, act.UserID) + } + if len(userIDs) == 0 { return nil } - log.Infof("questionInfo %s deleted will rollback activity %d", questionID, len(activityList)) - + // save activity _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - for _, act := range activityList { - log.Infof("user %s rollback rank %d", act.UserID, -act.Rank) - _, e := ar.userRankRepo.TriggerUserRank( - ctx, session, act.UserID, -act.Rank, act.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + session = session.Context(ctx) - if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + userInfoMapping, err := ar.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } + + err = ar.cancelActivities(session, activities) + if err != nil { + return nil, err + } + + err = ar.rollbackUserRank(ctx, session, activities, userInfoMapping) + if err != nil { + return nil, err } return nil, nil }) if err != nil { - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - // get all answers - answerList := make([]*entity.Answer, 0) - err = ar.data.DB.Find(&answerList, &entity.Answer{QuestionID: questionID}) + // notification + ar.sendCancelAcceptAnswerNotification(ctx, op) + return nil +} + +func (ar *AnswerActivityRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { + us := make([]*entity.User, 0) + err := session.In("id", userIDs).ForUpdate().Find(&us) if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + log.Error(err) + return nil, err } - for _, answerInfo := range answerList { - err = ar.DeleteAnswer(ctx, answerInfo.ID) - if err != nil { - log.Error(err) - } + + users := make(map[string]*entity.User, 0) + for _, u := range us { + users[u.ID] = u } - return + return users, nil } -// AcceptAnswer accept other answer -func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) { - addActivityList := make([]*entity.Activity, 0) - for _, action := range acceptActionList { - // get accept answer need add rank amount - activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action) - if e != nil { - return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - addActivity := &entity.Activity{ - ObjectID: answerObjID, - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, - } - if action == acceptAction { - addActivity.UserID = questionUserID - addActivity.TriggerUserID = converter.StringToInt64(answerUserID) - } else { - addActivity.UserID = answerUserID - addActivity.TriggerUserID = converter.StringToInt64(answerUserID) +// saveActivitiesAvailable save activities +// If activity not exist it will be created or else will be updated +// If this activity is already exist, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (ar *AnswerActivityRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.AcceptAnswerOperationInfo) ( + err error) { + for _, act := range op.Activities { + existsActivity := &entity.Activity{} + exist, err := session. + Where(builder.Eq{"object_id": op.AnswerObjectID}). + And(builder.Eq{"user_id": act.ActivityUserID}). + And(builder.Eq{"trigger_user_id": act.TriggerUserID}). + And(builder.Eq{"activity_type": act.ActivityType}). + Get(existsActivity) + if err != nil { + return err } - if isSelf { - addActivity.Rank = 0 + if exist && existsActivity.Cancelled == entity.ActivityAvailable { + act.Rank = 0 + continue } - addActivityList = append(addActivityList, addActivity) - } - - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - for _, addActivity := range addActivityList { - existsActivity, exists, e := ar.activityRepo.GetActivity( - ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() + if exist { + bean := &entity.Activity{ + Cancelled: entity.ActivityAvailable, + Rank: act.Rank, + HasRank: act.HasRank(), } - if exists && existsActivity.Cancelled == entity.ActivityAvailable { - continue - } - - reachStandard, e := ar.userRankRepo.TriggerUserRank( - ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() + session.Where("id = ?", existsActivity.ID) + if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`").Update(bean); err != nil { + return err } - if reachStandard { - addActivity.Rank = 0 + } else { + insertActivity := entity.Activity{ + ObjectID: op.AnswerObjectID, + OriginalObjectID: act.OriginalObjectID, + UserID: act.ActivityUserID, + TriggerUserID: converter.StringToInt64(act.TriggerUserID), + ActivityType: act.ActivityType, + Rank: act.Rank, + HasRank: act.HasRank(), + Cancelled: entity.ActivityAvailable, } - - if exists { - if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - } else { - if _, e = session.Insert(addActivity); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + _, err = session.Insert(&insertActivity) + if err != nil { + return err } } - return nil, nil - }) - if err != nil { - return err } - for _, act := range addActivityList { - msg := &schema.NotificationMsg{ - Type: schema.NotificationTypeAchievement, - ObjectID: act.ObjectID, - ReceiverUserID: act.UserID, + return nil +} + +// cancelActivities cancel activities +// If this activity is already cancelled, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (ar *AnswerActivityRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { + for _, act := range activities { + t := &entity.Activity{} + exist, err := session.ID(act.ID).Get(t) + if err != nil { + log.Error(err) + return err } - if act.UserID == questionUserID { - msg.TriggerUserID = answerUserID - msg.ObjectType = constant.AnswerObjectType - } else { - msg.TriggerUserID = questionUserID - msg.ObjectType = constant.AnswerObjectType + if !exist { + log.Error(fmt.Errorf("%s activity not exist", act.ID)) + return fmt.Errorf("%s activity not exist", act.ID) + } + // If this activity is already cancelled, set activity rank to 0 + if t.Cancelled == entity.ActivityCancelled { + act.Rank = 0 + } + if _, err = session.ID(act.ID).Cols("cancelled", "cancelled_at"). + Update(&entity.Activity{ + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), + }); err != nil { + log.Error(err) + return err } - notice_queue.AddNotification(msg) } + return nil +} - for _, act := range addActivityList { - msg := &schema.NotificationMsg{ - ReceiverUserID: act.UserID, - Type: schema.NotificationTypeInbox, - ObjectID: act.ObjectID, +func (ar *AnswerActivityRepo) changeUserRank(ctx context.Context, session *xorm.Session, + op *schema.AcceptAnswerOperationInfo, + userInfoMapping map[string]*entity.User) (err error) { + for _, act := range op.Activities { + if act.Rank == 0 { + continue } - if act.UserID != questionUserID { - msg.TriggerUserID = questionUserID - msg.ObjectType = constant.AnswerObjectType - msg.NotificationAction = constant.AdoptAnswer - notice_queue.AddNotification(msg) + user := userInfoMapping[act.ActivityUserID] + if user == nil { + continue + } + if err = ar.userRankRepo.ChangeUserRank(ctx, session, + act.ActivityUserID, user.Rank, act.Rank); err != nil { + log.Error(err) + return err } } - return err + return nil } -// CancelAcceptAnswer accept other answer -func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string) (err error) { - addActivityList := make([]*entity.Activity, 0) - for _, action := range acceptActionList { - // get accept answer need add rank amount - activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action) - if e != nil { - return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() +func (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, + activities []*entity.Activity, + userInfoMapping map[string]*entity.User) (err error) { + for _, act := range activities { + if act.Rank == 0 { + continue } - addActivity := &entity.Activity{ - ObjectID: answerObjID, - ActivityType: activityType, - Rank: -deltaRank, - HasRank: hasRank, + user := userInfoMapping[act.UserID] + if user == nil { + continue } - if action == acceptAction { - addActivity.UserID = questionUserID - } else { - addActivity.UserID = answerUserID + if err = ar.userRankRepo.ChangeUserRank(ctx, session, + act.UserID, user.Rank, -act.Rank); err != nil { + log.Error(err) + return err } - addActivityList = append(addActivityList, addActivity) } + return nil +} - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - for _, addActivity := range addActivityList { - existsActivity, exists, e := ar.activityRepo.GetActivity( - ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - if exists && existsActivity.Cancelled == entity.ActivityCancelled { - continue - } - if !exists { - continue - } +func (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) { + var activities []*entity.Activity + for _, action := range op.Activities { + var t []*entity.Activity + err := ar.data.DB.Context(ctx). + Where(builder.Eq{"user_id": action.ActivityUserID}). + And(builder.Eq{"activity_type": action.ActivityType}). + And(builder.Eq{"object_id": op.AnswerObjectID}). + Find(&t) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if len(t) > 0 { + activities = append(activities, t...) + } + } + return activities, nil +} - _, e = ar.userRankRepo.TriggerUserRank( - ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } +func (ar *AnswerActivityRepo) sendAcceptAnswerNotification( + ctx context.Context, op *schema.AcceptAnswerOperationInfo) { + for _, act := range op.Activities { + msg := &schema.NotificationMsg{ + Type: schema.NotificationTypeAchievement, + ObjectID: op.AnswerObjectID, + ReceiverUserID: act.ActivityUserID, + TriggerUserID: act.TriggerUserID, + } + msg.ObjectType = constant.AnswerObjectType + if msg.TriggerUserID != msg.ReceiverUserID { + ar.notificationQueueService.Send(ctx, msg) + } + } - if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + for _, act := range op.Activities { + msg := &schema.NotificationMsg{ + ReceiverUserID: act.ActivityUserID, + Type: schema.NotificationTypeInbox, + ObjectID: op.AnswerObjectID, + TriggerUserID: op.TriggerUserID, + } + if act.ActivityUserID != op.QuestionUserID { + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationAcceptAnswer + ar.notificationQueueService.Send(ctx, msg) } - return nil, nil - }) - if err != nil { - return err } - for _, act := range addActivityList { +} + +func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification( + ctx context.Context, op *schema.AcceptAnswerOperationInfo) { + for _, act := range op.Activities { msg := &schema.NotificationMsg{ - ReceiverUserID: act.UserID, + TriggerUserID: act.TriggerUserID, + ReceiverUserID: act.ActivityUserID, Type: schema.NotificationTypeAchievement, - ObjectID: act.ObjectID, + ObjectID: op.AnswerObjectID, } - if act.UserID == questionUserID { - msg.TriggerUserID = answerUserID + if act.ActivityUserID == op.QuestionObjectID { msg.ObjectType = constant.QuestionObjectType } else { - msg.TriggerUserID = questionUserID msg.ObjectType = constant.AnswerObjectType } - notice_queue.AddNotification(msg) - } - return err -} - -func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) { - answerInfo := &entity.Answer{} - exist, err := ar.data.DB.Where("id = ?", answerID).Get(answerInfo) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !exist { - return nil - } - - // get all this object activity - activityList := make([]*entity.Activity, 0) - session := ar.data.DB.Where("has_rank = 1") - session.Where("cancelled = ?", entity.ActivityAvailable) - err = session.Find(&activityList, &entity.Activity{ObjectID: answerID}) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if len(activityList) == 0 { - return nil - } - - log.Infof("answerInfo %s deleted will rollback activity %d", answerID, len(activityList)) - - _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - for _, act := range activityList { - log.Infof("user %s rollback rank %d", act.UserID, -act.Rank) - _, e := ar.userRankRepo.TriggerUserRank( - ctx, session, act.UserID, -act.Rank, act.ActivityType) - if e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } - - if _, e := session.Where("id = ?", act.ID).Cols("`cancelled`"). - Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - } + if msg.TriggerUserID != msg.ReceiverUserID { + ar.notificationQueueService.Send(ctx, msg) } - return nil, nil - }) - if err != nil { - return err } - return } diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go index 916459a2c..7af2189a6 100644 --- a/internal/repo/activity/follow_repo.go +++ b/internal/repo/activity/follow_repo.go @@ -1,18 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity import ( "context" + "time" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/follow" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/follow" + "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/log" "xorm.io/builder" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) @@ -37,13 +57,18 @@ func NewFollowRepo( } } -func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectId, "follow") +func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error { + objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) var ( existsActivity entity.Activity has bool @@ -51,15 +76,15 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"user_id": userId}). - And(builder.Eq{"object_id": objectId}). + And(builder.Eq{"user_id": userID}). + And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil { return } - if has && existsActivity.Cancelled == 0 { + if has && existsActivity.Cancelled == entity.ActivityAvailable { return } @@ -67,17 +92,18 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error _, err = session.Where(builder.Eq{"id": existsActivity.ID}). Cols(`cancelled`). Update(&entity.Activity{ - Cancelled: 0, + Cancelled: entity.ActivityAvailable, }) } else { // update existing activity with new user id and u object id _, err = session.Insert(&entity.Activity{ - UserID: userId, - ObjectID: objectId, - ActivityType: activityType, - Cancelled: 0, - Rank: 0, - HasRank: 0, + UserID: userID, + ObjectID: objectID, + OriginalObjectID: objectID, + ActivityType: activityType, + Cancelled: entity.ActivityAvailable, + Rank: 0, + HasRank: 0, }) } @@ -87,7 +113,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error } // start update followers when everything is fine - err = ar.updateFollows(ctx, session, objectId, 1) + err = ar.updateFollows(ctx, session, objectID, 1) if err != nil { log.Error(err) } @@ -98,13 +124,18 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error return err } -func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) error { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectId, "follow") +func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error { + objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { return err } _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) var ( existsActivity entity.Activity has bool @@ -112,42 +143,43 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"user_id": userId}). - And(builder.Eq{"object_id": objectId}). + And(builder.Eq{"user_id": userID}). + And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil || !has { return } - if has && existsActivity.Cancelled == 1 { + if has && existsActivity.Cancelled == entity.ActivityCancelled { return } if _, err = session.Where("id = ?", existsActivity.ID). Cols("cancelled"). Update(&entity.Activity{ - Cancelled: 1, + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), }); err != nil { return } - err = ar.updateFollows(ctx, session, objectId, -1) + err = ar.updateFollows(ctx, session, objectID, -1) return }) return err } -func (ar *FollowRepo) updateFollows(ctx context.Context, session *xorm.Session, objectId string, follows int) error { - objectType, err := obj.GetObjectTypeStrByObjectID(objectId) +func (ar *FollowRepo) updateFollows(ctx context.Context, session *xorm.Session, objectID string, follows int) error { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return err } switch objectType { case "question": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Question{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Question{}) case "user": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.User{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.User{}) case "tag": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Tag{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Tag{}) default: err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed") } diff --git a/internal/repo/activity/review_repo.go b/internal/repo/activity/review_repo.go new file mode 100644 index 000000000..c1f04b351 --- /dev/null +++ b/internal/repo/activity/review_repo.go @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/converter" + "xorm.io/builder" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/rank" + "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" +) + +// ReviewActivityRepo answer accepted +type ReviewActivityRepo struct { + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + configService *config.ConfigService +} + +const ( + EditAccepted = "edit.accepted" +) + +// NewReviewActivityRepo new repository +func NewReviewActivityRepo( + data *data.Data, + activityRepo activity_common.ActivityRepo, + userRankRepo rank.UserRankRepo, + configService *config.ConfigService, +) activity.ReviewActivityRepo { + return &ReviewActivityRepo{ + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + configService: configService, + } +} + +// Review user active +func (ar *ReviewActivityRepo) Review(ctx context.Context, act *schema.PassReviewActivity) (err error) { + cfg, err := ar.configService.GetConfigByKey(ctx, EditAccepted) + if err != nil { + return err + } + addActivity := &entity.Activity{ + UserID: act.UserID, + TriggerUserID: converter.StringToInt64(act.TriggerUserID), + ObjectID: act.ObjectID, + OriginalObjectID: act.OriginalObjectID, + ActivityType: cfg.ID, + Rank: cfg.GetIntValue(), + HasRank: 1, + RevisionID: converter.StringToInt64(act.RevisionID), + } + + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + user := &entity.User{} + exist, err := session.ID(addActivity.UserID).ForUpdate().Get(user) + if err != nil { + return nil, err + } + if !exist { + return nil, fmt.Errorf("user not exist") + } + + existsActivity := &entity.Activity{} + exist, err = session. + And(builder.Eq{"user_id": addActivity.UserID}). + And(builder.Eq{"activity_type": addActivity.ActivityType}). + And(builder.Eq{"revision_id": addActivity.RevisionID}). + Get(existsActivity) + if err != nil { + return nil, err + } + if exist { + return nil, nil + } + + err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) + if err != nil { + return nil, err + } + + _, err = session.Insert(addActivity) + if err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} diff --git a/internal/repo/activity/user_active_repo.go b/internal/repo/activity/user_active_repo.go index 27f6c5a6f..2452bcf83 100644 --- a/internal/repo/activity/user_active_repo.go +++ b/internal/repo/activity/user_active_repo.go @@ -1,25 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity import ( "context" + "fmt" + "xorm.io/builder" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/activity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/rank" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/rank" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // UserActiveActivityRepo answer accepted type UserActiveActivityRepo struct { - data *data.Data - activityRepo activity_common.ActivityRepo - userRankRepo rank.UserRankRepo - configRepo config.ConfigRepo + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + configService *config.ConfigService } const ( @@ -31,53 +52,68 @@ func NewUserActiveActivityRepo( data *data.Data, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, - configRepo config.ConfigRepo, + configService *config.ConfigService, ) activity.UserActiveActivityRepo { return &UserActiveActivityRepo{ - data: data, - activityRepo: activityRepo, - userRankRepo: userRankRepo, - configRepo: configRepo, + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + configService: configService, } } -// UserActive accept other answer +// UserActive user active func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) { + cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated) + if err != nil { + return err + } + addActivity := &entity.Activity{ + UserID: userID, + ObjectID: "0", + OriginalObjectID: "0", + ActivityType: cfg.ID, + Rank: cfg.GetIntValue(), + HasRank: 1, + } + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) - activityType, err := ar.configRepo.GetConfigType(UserActivated) + user := &entity.User{} + exist, err := session.ID(userID).ForUpdate().Get(user) if err != nil { return nil, err } - deltaRank, err := ar.configRepo.GetInt(UserActivated) - if err != nil { - return nil, err + if !exist { + return nil, fmt.Errorf("user not exist") } - addActivity := &entity.Activity{ - UserID: userID, - ObjectID: "0", - ActivityType: activityType, - Rank: deltaRank, - HasRank: 1, - } - _, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType) + existsActivity := &entity.Activity{} + exist, err = session. + And(builder.Eq{"user_id": addActivity.UserID}). + And(builder.Eq{"activity_type": addActivity.ActivityType}). + Get(existsActivity) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } - if exists { + if exist { return nil, nil } - _, err = ar.userRankRepo.TriggerUserRank(ctx, session, addActivity.UserID, addActivity.Rank, activityType) + err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } + _, err = session.Insert(addActivity) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } return nil, nil }) - return err + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil } diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index bcc95f934..f2d2be5f8 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -1,434 +1,507 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity import ( "context" - "github.com/answerdev/answer/pkg/converter" - "strings" + "fmt" + "time" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/notice_queue" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/service/content" + "github.com/segmentfault/pacman/log" - "xorm.io/builder" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/pkg/converter" + + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/pkg/obj" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" + "xorm.io/builder" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" "github.com/segmentfault/pacman/errors" "xorm.io/xorm" ) // VoteRepo activity repository type VoteRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - configRepo config.ConfigRepo - activityRepo activity_common.ActivityRepo - userRankRepo rank.UserRankRepo - voteCommon activity_common.VoteRepo + data *data.Data + activityRepo activity_common.ActivityRepo + userRankRepo rank.UserRankRepo + notificationQueueService notice_queue.NotificationQueueService } // NewVoteRepo new repository func NewVoteRepo( data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, - configRepo config.ConfigRepo, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, - voteCommon activity_common.VoteRepo) service.VoteRepo { + notificationQueueService notice_queue.NotificationQueueService, +) content.VoteRepo { return &VoteRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - configRepo: configRepo, - activityRepo: activityRepo, - userRankRepo: userRankRepo, - voteCommon: voteCommon, + data: data, + activityRepo: activityRepo, + userRankRepo: userRankRepo, + notificationQueueService: notificationQueueService, } } -var LimitUpActions = map[string][]string{ - "question": {"vote_up", "voted_up"}, - "answer": {"vote_up", "voted_up"}, - "comment": {"vote_up"}, -} +func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + noNeedToVote, err := vr.votePreCheck(ctx, op) + if err != nil { + return err + } + if noNeedToVote { + return nil + } -var LimitDownActions = map[string][]string{ - "question": {"vote_down", "voted_down"}, - "answer": {"vote_down", "voted_down"}, - "comment": {"vote_down"}, -} + sendInboxNotification := false + maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx) + if err != nil { + return err + } + var userIDs []string + for _, activity := range op.Activities { + userIDs = append(userIDs, activity.ActivityUserID) + } -func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserId string, actions []string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - result = nil - for _, action := range actions { - var ( - existsActivity entity.Activity - insertActivity entity.Activity - has bool - triggerUserID, - activityUserId string - activityType, deltaRank, hasRank int - ) - - activityUserId, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserId, userID, action) - if err != nil { - return - } + session = session.Context(ctx) - triggerUserID = userID - if userID == activityUserId { - triggerUserID = "0" - } - - // check is voted up - has, _ = session. - Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"user_id": activityUserId}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - Get(&existsActivity) - - // is is voted,return - if has && existsActivity.Cancelled == 0 { - return - } - - insertActivity = entity.Activity{ - ObjectID: objectID, - UserID: activityUserId, - TriggerUserID: converter.StringToInt64(triggerUserID), - ActivityType: activityType, - Rank: deltaRank, - HasRank: hasRank, - Cancelled: 0, - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } - // trigger user rank and send notification - if hasRank != 0 { - isReachStandard, err := vr.userRankRepo.TriggerUserRank(ctx, session, activityUserId, deltaRank, activityType) - if err != nil { - return nil, err - } - if isReachStandard { - insertActivity.Rank = 0 - } - - vr.sendNotification(ctx, activityUserId, objectUserId, objectID) - } + err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, userInfoMapping, maxDailyRank) + if err != nil { + return nil, err + } - if has { - if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{ - Cancelled: 0, - }); err != nil { - return - } - } else { - _, err = session.Insert(&insertActivity) - if err != nil { - return nil, err - } - } + sendInboxNotification, err = vr.saveActivitiesAvailable(session, op) + if err != nil { + return nil, err + } - // update votes - if action == "vote_down" || action == "vote_up" { - votes := 1 - if action == "vote_down" { - votes = -1 - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + err = vr.changeUserRank(ctx, session, op, userInfoMapping) + if err != nil { + return nil, err } - return + return nil, nil }) if err != nil { - return + return err } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - - return + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID) + } + if sendInboxNotification { + vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp) + } + return nil } -func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserId string, actions []string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { - for _, action := range actions { - var ( - existsActivity entity.Activity - has bool - triggerUserID, - activityUserId string - activityType, - deltaRank, hasRank int - ) - result = nil - - activityUserId, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserId, userID, action) - if err != nil { - return - } - - triggerUserID = userID - if userID == activityUserId { - triggerUserID = "0" - } - - has, err = session. - Where(builder.Eq{"user_id": activityUserId}). - And(builder.Eq{"trigger_user_id": triggerUserID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"object_id": objectID}). - Get(&existsActivity) - - if !has { - return - } - - if existsActivity.Cancelled == 1 { - return - } - - if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). - Update(&entity.Activity{ - Cancelled: 1, - }); err != nil { - return - } +func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) { + // Pre-Check + // 1. check if the activity exist + // 2. check if the activity is not cancelled + // 3. if all activities are cancelled, return directly + activities, err := vr.getExistActivity(ctx, op) + if err != nil { + return err + } + var userIDs []string + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + userIDs = append(userIDs, activity.UserID) + } + if len(userIDs) == 0 { + return nil + } - // trigger user rank and send notification - if hasRank != 0 { - _, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserId, -deltaRank, activityType) - if err != nil { - return - } + _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) - vr.sendNotification(ctx, activityUserId, objectUserId, objectID) - } + userInfoMapping, err := vr.acquireUserInfo(session, userIDs) + if err != nil { + return nil, err + } - // update votes - if action == "vote_down" || action == "vote_up" { - votes := -1 - if action == "vote_down" { - votes = 1 - } - err = vr.updateVotes(ctx, session, objectID, votes) - if err != nil { - return - } - } + err = vr.cancelActivities(session, activities) + if err != nil { + return nil, err } - return + err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping) + if err != nil { + return nil, err + } + return nil, nil }) if err != nil { - return - } - resp, err = vr.GetVoteResultByObjectId(ctx, objectID) - resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) - return -} - -func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return + return err } - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID) } - - _, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserId) - return vr.vote(ctx, objectID, userID, objectUserId, actions) + return nil } -func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - objectType, err := obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return - } - - _, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserId) - return vr.vote(ctx, objectID, userID, objectUserId, actions) +func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) ( + up, down int64, err error) { + up = vr.countVoteUp(ctx, objectID, objectType) + down = vr.countVoteDown(ctx, objectID, objectType) + err = vr.updateVotes(ctx, objectID, objectType, int(up-down)) + return } -func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - var ( - objectType string - ) - resp = &schema.VoteResp{} +func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string, + page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) { + session := vr.data.DB.Context(ctx) + cond := builder. + And( + builder.Eq{"user_id": userID}, + builder.Eq{"cancelled": 0}, + builder.In("activity_type", activityTypes), + ) + + session.Where(cond).Desc("updated_at") - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) + total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session) if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - actions, ok := LimitUpActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - - return vr.voteCancel(ctx, objectID, userID, objectUserId, actions) + return } -func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - var ( - objectType string - ) - resp = &schema.VoteResp{} - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) { + activities, err := vr.getExistActivity(ctx, op) if err != nil { - err = errors.BadRequest(reason.ObjectNotFound) - return + return false, err } - actions, ok := LimitDownActions[objectType] - if !ok { - err = errors.BadRequest(reason.DisallowVote) - return + done := 0 + for _, activity := range activities { + if activity.Cancelled == entity.ActivityAvailable { + done++ + } } - - return vr.voteCancel(ctx, objectID, userID, objectUserId, actions) + return done == len(op.Activities), nil } -func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserId, userID string, action string) (activityUserId string, activityType, rank, hasRank int, err error) { - activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) - +func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) { + us := make([]*entity.User, 0) + err := session.In("id", userIDs).ForUpdate().Find(&us) if err != nil { - return + log.Error(err) + return nil, err } - activityUserId = userID - if strings.Contains(action, "voted") { - activityUserId = objectUserId + users := make(map[string]*entity.User, 0) + for _, u := range us { + users[u.ID] = u } - - return activityUserId, activityType, rank, hasRank, nil + return users, nil } -func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) { - resp = &schema.VoteResp{} - for _, action := range []string{"vote_up", "vote_down"} { - var ( - activity entity.Activity - votes int64 - activityType int - ) +func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) { + // check if user reach daily rank limit + for _, activity := range op.Activities { + if userInfoMapping[activity.ActivityUserID] == nil { + continue + } + if activity.Rank > 0 { + // check if reach max daily rank + reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) + if err != nil { + log.Error(err) + return err + } + if reach { + activity.Rank = 0 + continue + } + } else { + // If user rank is lower than 1 after this action, then user rank will be set to 1 only. + userCurrentScore := userInfoMapping[activity.ActivityUserID].Rank + if userCurrentScore+activity.Rank < 1 { + activity.Rank = 1 - userCurrentScore + } + } + } + return nil +} - activityType, _, _, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) +func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session, + op *schema.VoteOperationInfo, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range op.Activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.ActivityUserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.ActivityUserID, user.Rank, activity.Rank); err != nil { + log.Error(err) + return err + } + } + return nil +} - votes, err = vr.data.DB.Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"cancelled": 0}). - Count(&activity) +func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session, + activities []*entity.Activity, + userInfoMapping map[string]*entity.User) (err error) { + for _, activity := range activities { + if activity.Rank == 0 { + continue + } + user := userInfoMapping[activity.UserID] + if user == nil { + continue + } + if err = vr.userRankRepo.ChangeUserRank(ctx, session, + activity.UserID, user.Rank, -activity.Rank); err != nil { + log.Error(err) + return err + } + } + return nil +} +// saveActivitiesAvailable save activities +// If activity not exist it will be created or else will be updated +// If this activity is already exist, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) { + for _, activity := range op.Activities { + existsActivity := &entity.Activity{} + exist, err := session. + Where(builder.Eq{"object_id": op.ObjectID}). + And(builder.Eq{"user_id": activity.ActivityUserID}). + And(builder.Eq{"trigger_user_id": activity.TriggerUserID}). + And(builder.Eq{"activity_type": activity.ActivityType}). + Get(existsActivity) if err != nil { - return + return false, err } - - if action == "vote_up" { - resp.UpVotes = int(votes) + if exist && existsActivity.Cancelled == entity.ActivityAvailable { + activity.Rank = 0 + continue + } + if exist { + bean := &entity.Activity{ + Cancelled: entity.ActivityAvailable, + Rank: activity.Rank, + HasRank: activity.HasRank(), + } + session.Where("id = ?", existsActivity.ID) + if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`"). + Update(bean); err != nil { + return false, err + } } else { - resp.DownVotes = int(votes) + insertActivity := entity.Activity{ + ObjectID: op.ObjectID, + OriginalObjectID: op.ObjectID, + UserID: activity.ActivityUserID, + TriggerUserID: converter.StringToInt64(activity.TriggerUserID), + ActivityType: activity.ActivityType, + Rank: activity.Rank, + HasRank: activity.HasRank(), + Cancelled: entity.ActivityAvailable, + } + _, err = session.Insert(&insertActivity) + if err != nil { + return false, err + } + newAct = true } } + return newAct, nil +} - resp.Votes = resp.UpVotes - resp.DownVotes +// cancelActivities cancel activities +// If this activity is already cancelled, set activity rank to 0 +// So after this function, the activity rank will be correct for update user rank +func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) { + for _, activity := range activities { + t := &entity.Activity{} + exist, err := session.ID(activity.ID).Get(t) + if err != nil { + log.Error(err) + return err + } + if !exist { + log.Error(fmt.Errorf("%s activity not exist", activity.ID)) + return fmt.Errorf("%s activity not exist", activity.ID) + } + // If this activity is already cancelled, set activity rank to 0 + if t.Cancelled == entity.ActivityCancelled { + activity.Rank = 0 + } + if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at"). + Update(&entity.Activity{ + Cancelled: entity.ActivityCancelled, + CancelledAt: time.Now(), + }); err != nil { + log.Error(err) + return err + } + } + return nil +} - return resp, nil +func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) { + var activities []*entity.Activity + for _, action := range op.Activities { + t := &entity.Activity{} + exist, err := vr.data.DB.Context(ctx). + Where(builder.Eq{"user_id": action.ActivityUserID}). + And(builder.Eq{"trigger_user_id": action.TriggerUserID}). + And(builder.Eq{"activity_type": action.ActivityType}). + And(builder.Eq{"object_id": op.ObjectID}). + Get(t) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + activities = append(activities, t) + } + } + return activities, nil } -func (vr *VoteRepo) ListUserVotes( - ctx context.Context, - userID string, - req schema.GetVoteWithPageReq, - activityTypes []int, -) (voteList []entity.Activity, total int64, err error) { - session := vr.data.DB.NewSession() - cond := builder. - And( - builder.Eq{"user_id": userID}, - builder.Eq{"cancelled": 0}, - builder.In("activity_type", activityTypes), - ) +func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp) + if err != nil { + log.Errorf("get vote up count error: %v", err) + } + return count +} - session.Where(cond).OrderBy("updated_at desc") +func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) { + count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown) + if err != nil { + log.Errorf("get vote down count error: %v", err) + } + return count +} - total, err = pager.Help(req.Page, req.PageSize, &voteList, &entity.Activity{}, session) +func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) { + activity := &entity.Activity{} + activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action) + count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}). + And(builder.Eq{"activity_type": activityType}). + And(builder.Eq{"cancelled": 0}). + Count(activity) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return + return count, err } -// updateVotes -// if votes < 0 Decr object vote_count,otherwise Incr object vote_count -func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) { - var ( - objectType string - e error - ) - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) +func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) { + session := vr.data.DB.Context(ctx) switch objectType { - case "question": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{}) - case "answer": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{}) - case "comment": - _, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{}) - default: - e = errors.BadRequest(reason.DisallowVote) + case constant.QuestionObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount}) + case constant.AnswerObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount}) + case constant.CommentObjectType: + _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount}) } - - if e != nil { - err = e - } else if err != nil { - err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack() + if err != nil { + log.Error(err) } - return } -// sendNotification send rank triggered notification -func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserId, objectUserId, objectID string) { +func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } msg := &schema.NotificationMsg{ - ReceiverUserID: activityUserId, - TriggerUserID: objectUserId, + ReceiverUserID: activityUserID, + TriggerUserID: objectUserID, Type: schema.NotificationTypeAchievement, ObjectID: objectID, ObjectType: objectType, } - notice_queue.AddNotification(msg) + vr.notificationQueueService.Send(ctx, msg) +} + +func (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) { + if triggerUserID == receiverUserID { + return + } + objectType, _ := obj.GetObjectTypeStrByObjectID(objectID) + + msg := &schema.NotificationMsg{ + TriggerUserID: triggerUserID, + ReceiverUserID: receiverUserID, + Type: schema.NotificationTypeInbox, + ObjectID: objectID, + ObjectType: objectType, + } + if objectType == constant.QuestionObjectType { + if upvote { + msg.NotificationAction = constant.NotificationUpVotedTheQuestion + } else { + msg.NotificationAction = constant.NotificationDownVotedTheQuestion + } + } + if objectType == constant.AnswerObjectType { + if upvote { + msg.NotificationAction = constant.NotificationUpVotedTheAnswer + } else { + msg.NotificationAction = constant.NotificationDownVotedTheAnswer + } + } + if objectType == constant.CommentObjectType { + if upvote { + msg.NotificationAction = constant.NotificationUpVotedTheComment + } + } + if len(msg.NotificationAction) > 0 { + vr.notificationQueueService.Send(ctx, msg) + } } diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go new file mode 100644 index 000000000..cf2d596e9 --- /dev/null +++ b/internal/repo/activity_common/activity_repo.go @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity_common + +import ( + "context" + "fmt" + "time" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_type" + "github.com/apache/answer/pkg/obj" + "xorm.io/builder" + "xorm.io/xorm" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" +) + +// ActivityRepo activity repository +type ActivityRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo + configService *config.ConfigService +} + +// NewActivityRepo new repository +func NewActivityRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, + configService *config.ConfigService, +) activity_common.ActivityRepo { + return &ActivityRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + configService: configService, + } +} + +func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) ( + activityType, rank, hasRank int, err error) { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return + } + + confKey := fmt.Sprintf("%s.%s", objectType, action) + cfg, err := ar.configService.GetConfigByKey(ctx, confKey) + if err != nil { + return + } + activityType, rank = cfg.ID, cfg.GetIntValue() + hasRank = 0 + if rank != 0 { + hasRank = 1 + } + return +} + +func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) { + configKey := fmt.Sprintf("%s.%s", objectType, action) + cfg, err := ar.configService.GetConfigByKey(ctx, configKey) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return cfg.ID, nil +} + +func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) { + cfg, err := ar.configService.GetConfigByKey(ctx, configKey) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return cfg.ID, nil +} + +func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, + objectID, userID string, activityType int, +) (existsActivity *entity.Activity, exist bool, err error) { + existsActivity = &entity.Activity{} + exist, err = session. + Where(builder.Eq{"object_id": objectID}). + And(builder.Eq{"user_id": userID}). + And(builder.Eq{"activity_type": activityType}). + Get(existsActivity) + return +} + +func (ar *ActivityRepo) GetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) ( + activityList []*entity.Activity, err error) { + activityList = make([]*entity.Activity, 0) + err = ar.data.DB.Context(ctx).Where("user_id = ?", userID). + And("activity_type = ?", activityType). + And("cancelled = 0"). + Find(&activityList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { + sum := &entity.ActivityRankSum{} + _, err := ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()). + Select("sum(`rank`) as `rank`"). + Where("user_id =?", userID). + And("object_id = ?", objectID). + And("cancelled =0"). + Get(sum) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return 0, err + } + return sum.Rank, nil +} + +// AddActivity add activity +func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) { + _, err = ar.data.DB.Context(ctx).Insert(activity) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time +func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation( + ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) { + rankStat = make([]*entity.ActivityUserRankStat, 0) + session := ar.data.DB.Context(ctx).Select("user_id, SUM(`rank`) AS rank_amount").Table("activity") + session.Where("has_rank = 1 AND cancelled = 0") + session.Where("created_at >= ?", startTime) + session.Where("created_at <= ?", endTime) + session.GroupBy("user_id") + session.Desc("rank_amount") + session.Limit(limit) + err = session.Find(&rankStat) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUsersWhoHasVoteMost get users who has vote most +func (ar *ActivityRepo) GetUsersWhoHasVoteMost( + ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) { + voteStat = make([]*entity.ActivityUserVoteStat, 0) + + actIDs := make([]int, 0) + for _, act := range activity_type.ActivityTypeList { + cfg, err := ar.configService.GetConfigByKey(ctx, act) + if err == nil { + actIDs = append(actIDs, cfg.ID) + } + } + + session := ar.data.DB.Context(ctx).Select("user_id, COUNT(*) AS vote_count").Table("activity") + session.Where("cancelled = 0") + session.In("activity_type", actIDs) + session.Where("created_at >= ?", startTime) + session.Where("created_at <= ?", endTime) + session.GroupBy("user_id") + session.Desc("vote_count") + session.Limit(limit) + err = session.Find(&voteStat) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index a793fdec5..99e5a6e67 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -1,15 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity_common import ( "context" + "time" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" + "xorm.io/xorm" ) // FollowRepo follow repository @@ -33,27 +56,27 @@ func NewFollowRepo( } // GetFollowAmount get object id's follows -func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectId string) (follows int, err error) { - objectType, err := obj.GetObjectTypeStrByObjectID(objectId) +func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return 0, err } switch objectType { case "question": model := &entity.Question{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "user": model := &entity.User{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "tag": model := &entity.Tag{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } @@ -71,15 +94,16 @@ func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectId string) (fol func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) { objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow") if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + log.Errorf("can't get activity type by object key: %s", objectTypeStr) + return nil, err } userIDs = make([]string, 0) - session := ar.data.DB.Select("user_id") + session := ar.data.DB.Context(ctx).Select("user_id") session.Table(entity.Activity{}.TableName()) session.Where("object_id = ?", objectID) session.Where("activity_type = ?", activityType) @@ -94,8 +118,11 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us // GetFollowIDs get all follow id list func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { followIDs = make([]string, 0) - activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") - session := ar.data.DB.Select("object_id") + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + session := ar.data.DB.Context(ctx).Select("object_id") session.Table(entity.Activity{}.TableName()) session.Where("user_id = ? AND activity_type = ?", userID, activityType) session.Where("cancelled = 0") @@ -107,23 +134,132 @@ func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string } // IsFollowed check user if follow object or not -func (ar *FollowRepo) IsFollowed(userId, objectId string) (bool, error) { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(nil, objectId, "follow") +func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (followed bool, err error) { + objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return false, err + } + + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow") if err != nil { return false, err } at := &entity.Activity{} - has, err := ar.data.DB.Where("user_id = ? AND object_id = ? AND activity_type = ?", userId, objectId, activityType).Get(at) + has, err := ar.data.DB.Context(ctx).Where("user_id = ? AND object_id = ? AND activity_type = ?", userID, objectID, activityType).Get(at) if err != nil { return false, err } if !has { return false, nil } - if at.Cancelled == 1 { + if at.Cancelled == entity.ActivityCancelled { return false, nil } else { return true, nil } } + +// MigrateFollowers migrate followers from source object to target object +func (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error { + // if source object id and target object id are same type + sourceObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(sourceObjectID) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + targetObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(targetObjectID) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if sourceObjectTypeStr != targetObjectTypeStr { + return errors.InternalServer(reason.DisallowFollow).WithMsg("not same object type") + } + activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, sourceObjectTypeStr, action) + if err != nil { + return err + } + + // 1. get all user ids who follow the source object + userIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID) + if err != nil { + log.Errorf("MigrateFollowers: failed to get user ids who follow %s: %v", sourceObjectID, err) + return err + } + + _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + // 1. delete all follows of the source object + _, err = session.Table(entity.Activity{}.TableName()). + Where(builder.Eq{ + "object_id": sourceObjectID, + "activity_type": activityType, + }). + Delete(&entity.Activity{}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // 2. update cancel status to active for target tag if source tag followers is active + _, err = session.Table(entity.Activity{}.TableName()). + Where(builder.Eq{ + "object_id": targetObjectID, + "activity_type": activityType, + }). + And(builder.In("user_id", userIDs)). + Cols("cancelled"). + Update(&entity.Activity{ + Cancelled: entity.ActivityAvailable, + }) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // 3. get existing follows of the target object + targetFollowers := make([]string, 0) + err = session.Table(entity.Activity{}.TableName()). + Where(builder.Eq{ + "object_id": targetObjectID, + "activity_type": activityType, + "cancelled": entity.ActivityAvailable, + }). + Cols("user_id"). + Find(&targetFollowers) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // 4. filter out user ids that already follow the target object and create new activity + // Create a map for faster lookup of existing followers + existingFollowers := make(map[string]bool) + for _, uid := range targetFollowers { + existingFollowers[uid] = true + } + + // Filter out users who already follow the target + newFollowers := make([]string, 0) + for _, uid := range userIDs { + if !existingFollowers[uid] { + newFollowers = append(newFollowers, uid) + } + } + + // Create new activities for the filtered users + for _, uid := range newFollowers { + activity := &entity.Activity{ + UserID: uid, + ObjectID: targetObjectID, + OriginalObjectID: targetObjectID, + ActivityType: activityType, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Cancelled: entity.ActivityAvailable, + } + if _, err = session.Insert(activity); err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + } + return nil, nil + }) + + return err +} diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index c2525145a..cb7d23d00 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -1,18 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity_common import ( "context" + "github.com/apache/answer/pkg/uid" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) // VoteRepo activity repository type VoteRepo struct { data *data.Data - uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo } @@ -24,12 +45,18 @@ func NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) act } } -func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectId, userId string) (status string) { +func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) { + objectID = uid.DeShortID(objectID) for _, action := range []string{"vote_up", "vote_down"} { + activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) + if err != nil { + return "" + } at := &entity.Activity{} - activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectId, action) - has, err := vr.data.DB.Where("object_id =? AND cancelled=0 AND activity_type=? AND user_id=?", objectId, activityType, userId).Get(at) + has, err := vr.data.DB.Context(ctx).Where("object_id = ? AND cancelled = 0 AND activity_type = ? AND user_id = ?", + objectID, activityType, userID).Get(at) if err != nil { + log.Error(err) return "" } if has { @@ -38,3 +65,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectId, userId string) } return "" } + +func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) { + list := make([]*entity.Activity, 0) + count, err = vr.data.DB.Context(ctx).Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/activity_repo.go b/internal/repo/activity_repo.go deleted file mode 100644 index df14ae019..000000000 --- a/internal/repo/activity_repo.go +++ /dev/null @@ -1,90 +0,0 @@ -package repo - -import ( - "context" - "fmt" - - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/pkg/obj" - "xorm.io/builder" - "xorm.io/xorm" - - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/unique" - "github.com/segmentfault/pacman/errors" -) - -// ActivityRepo activity repository -type ActivityRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - configRepo config.ConfigRepo -} - -// NewActivityRepo new repository -func NewActivityRepo( - data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, - configRepo config.ConfigRepo, -) activity_common.ActivityRepo { - return &ActivityRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - configRepo: configRepo, - } -} - -func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank, hasRank int, err error) { - objectKey, err := obj.GetObjectTypeStrByObjectID(objectId) - if err != nil { - return - } - - confKey := fmt.Sprintf("%s.%s", objectKey, action) - activityType, err = ar.configRepo.GetConfigType(confKey) - - rank, err = ar.configRepo.GetInt(confKey) - hasRank = 0 - if rank != 0 { - hasRank = 1 - } - return -} - -func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) { - confKey := fmt.Sprintf("%s.%s", objectKey, action) - activityType, err = ar.configRepo.GetConfigType(confKey) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, - objectID, userID string, activityType int) (existsActivity *entity.Activity, exist bool, err error) { - existsActivity = &entity.Activity{} - exist, err = session. - Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"user_id": userID}). - And(builder.Eq{"activity_type": activityType}). - Get(existsActivity) - return -} - -func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { - sum := &entity.ActivityRankSum{} - _, err := ar.data.DB.Table(entity.Activity{}.TableName()). - Select("sum(rank) as rank"). - Where("user_id =?", userID). - And("object_id = ?", objectID). - And("cancelled =0"). - Get(sum) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return 0, err - } - return sum.Rank, nil -} diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go new file mode 100644 index 000000000..c5447befc --- /dev/null +++ b/internal/repo/answer/answer_repo.go @@ -0,0 +1,554 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package answer + +import ( + "context" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// answerRepo answer repository +type answerRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo + userRankRepo rank.UserRankRepo + activityRepo activity_common.ActivityRepo +} + +// NewAnswerRepo new repository +func NewAnswerRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, + userRankRepo rank.UserRankRepo, + activityRepo activity_common.ActivityRepo, +) answercommon.AnswerRepo { + return &answerRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + userRankRepo: userRankRepo, + activityRepo: activityRepo, + } +} + +// AddAnswer add answer +func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) { + answer.QuestionID = uid.DeShortID(answer.QuestionID) + ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName()) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + answer.ID = ID + _, err = ar.data.DB.Context(ctx).Insert(answer) + + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + answer.ID = uid.EnShortID(answer.ID) + answer.QuestionID = uid.EnShortID(answer.QuestionID) + } + _ = ar.updateSearch(ctx, answer.ID) + return nil +} + +// RemoveAnswer delete answer +func (ar *answerRepo) RemoveAnswer(ctx context.Context, answerID string) (err error) { + answerID = uid.DeShortID(answerID) + _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ + Status: entity.AnswerStatusDeleted, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = ar.updateSearch(ctx, answerID) + return nil +} + +// RecoverAnswer recover answer +func (ar *answerRepo) RecoverAnswer(ctx context.Context, answerID string) (err error) { + answerID = uid.DeShortID(answerID) + _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{ + Status: entity.AnswerStatusAvailable, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = ar.updateSearch(ctx, answerID) + return nil +} + +// RemoveAllUserAnswer remove all user answer +func (ar *answerRepo) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) { + // find all answer id that need to be deleted + answerIDs := make([]string, 0) + session := ar.data.DB.Context(ctx).Where("user_id = ?", userID) + session.Where("status != ?", entity.AnswerStatusDeleted) + err = session.Select("id").Table("answer").Find(&answerIDs) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if len(answerIDs) == 0 { + return nil + } + + log.Infof("find %d answers need to be deleted for user %s", len(answerIDs), userID) + + // delete all question + session = ar.data.DB.Context(ctx).Where("user_id = ?", userID) + session.Where("status != ?", entity.AnswerStatusDeleted) + _, err = session.Cols("status", "updated_at").Update(&entity.Answer{ + UpdatedAt: time.Now(), + Status: entity.AnswerStatusDeleted, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // update search content + for _, id := range answerIDs { + _ = ar.updateSearch(ctx, id) + } + return nil +} + +// UpdateAnswer update answer +func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) { + answer.ID = uid.DeShortID(answer.ID) + answer.QuestionID = uid.DeShortID(answer.QuestionID) + _, err = ar.data.DB.Context(ctx).ID(answer.ID).Cols(cols...).Update(answer) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = ar.updateSearch(ctx, answer.ID) + return err +} + +func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) { + answerID = uid.DeShortID(answerID) + _, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{Status: status}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = ar.updateSearch(ctx, answerID) + return +} + +// GetAnswer get answer one +func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( + answer *entity.Answer, exist bool, err error, +) { + id = uid.DeShortID(id) + answer = &entity.Answer{} + exist, err = ar.data.DB.Context(ctx).ID(id).Get(answer) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + answer.ID = uid.EnShortID(answer.ID) + answer.QuestionID = uid.EnShortID(answer.QuestionID) + } + return +} + +// GetAnswerCount count answer +func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { + var resp = new(entity.Answer) + count, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusAvailable).Count(resp) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetAnswerList get answer list all +func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { + answerList = make([]*entity.Answer, 0) + answer.ID = uid.DeShortID(answer.ID) + answer.QuestionID = uid.DeShortID(answer.QuestionID) + err = ar.data.DB.Context(ctx).Find(&answerList, answer) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range answerList { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return +} + +// GetAnswerPage get answer page +func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) { + answer.ID = uid.DeShortID(answer.ID) + answer.QuestionID = uid.DeShortID(answer.QuestionID) + answerList = make([]*entity.Answer, 0) + total, err = pager.Help(page, pageSize, &answerList, answer, ar.data.DB.Context(ctx)) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range answerList { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return +} + +// UpdateAcceptedStatus update all accepted status of this question's answers +func (ar *answerRepo) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error { + acceptedAnswerID = uid.DeShortID(acceptedAnswerID) + questionID = uid.DeShortID(questionID) + + // update all this question's answer accepted status to false + _, err := ar.data.DB.Context(ctx).Where("question_id = ?", questionID).Cols("adopted").Update(&entity.Answer{ + Accepted: schema.AnswerAcceptedFailed, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // if acceptedAnswerID is not empty, update accepted status to true + if len(acceptedAnswerID) > 0 && acceptedAnswerID != "0" { + _, err = ar.data.DB.Context(ctx).Where("id = ?", acceptedAnswerID).Cols("adopted").Update(&entity.Answer{ + Accepted: schema.AnswerAcceptedEnable, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + } + _ = ar.updateSearch(ctx, acceptedAnswerID) + return nil +} + +// GetByID +func (ar *answerRepo) GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) { + var resp entity.Answer + answerID = uid.DeShortID(answerID) + has, err := ar.data.DB.Context(ctx).ID(answerID).Get(&resp) + if err != nil { + return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + resp.ID = uid.EnShortID(resp.ID) + resp.QuestionID = uid.EnShortID(resp.QuestionID) + } + return &resp, has, nil +} + +func (ar *answerRepo) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) { + for idx, answerID := range answerIDs { + answerIDs[idx] = uid.DeShortID(answerID) + } + var resp = make([]*entity.Answer, 0) + err := ar.data.DB.Context(ctx).In("id", answerIDs).Find(&resp) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range resp { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return resp, nil +} + +func (ar *answerRepo) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) { + questionID = uid.DeShortID(questionID) + var resp = new(entity.Answer) + count, err := ar.data.DB.Context(ctx).Where("question_id =? and status = ?", questionID, entity.AnswerStatusAvailable).Count(resp) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (ar *answerRepo) GetCountByUserID(ctx context.Context, userID string) (int64, error) { + var resp = new(entity.Answer) + count, err := ar.data.DB.Context(ctx).Where(" user_id = ? and status = ?", userID, entity.AnswerStatusAvailable).Count(resp) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (ar *answerRepo) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) { + questionID = uid.DeShortID(questionID) + var ids []string + resp := make([]string, 0) + err := ar.data.DB.Context(ctx).Table(entity.Answer{}.TableName()).Where("question_id =? and user_id = ? and status = ?", questionID, userID, entity.AnswerStatusAvailable).OrderBy("created_at ASC").Cols("id").Find(&ids) + if err != nil { + return resp, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, id := range ids { + resp = append(resp, uid.EnShortID(id)) + } + } else { + resp = ids + } + return resp, nil +} + +// SearchList +func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { + if search.QuestionID != "" { + search.QuestionID = uid.DeShortID(search.QuestionID) + } + search.ID = uid.DeShortID(search.ID) + var count int64 + var err error + rows := make([]*entity.Answer, 0) + if search.Page > 0 { + search.Page = search.Page - 1 + } else { + search.Page = 0 + } + if search.PageSize == 0 { + search.PageSize = constant.DefaultPageSize + } + offset := search.Page * search.PageSize + session := ar.data.DB.Context(ctx) + + if search.QuestionID != "" { + session = session.And("question_id = ?", search.QuestionID) + } + if len(search.UserID) > 0 { + session = session.And("user_id = ?", search.UserID) + } + switch search.Order { + case entity.AnswerSearchOrderByTime: + session = session.OrderBy("created_at desc") + case entity.AnswerSearchOrderByTimeAsc: + session = session.OrderBy("created_at asc") + case entity.AnswerSearchOrderByVote: + session = session.OrderBy("vote_count desc") + default: + session = session.OrderBy("adopted desc,vote_count desc,created_at asc") + } + if !search.IncludeDeleted { + if search.LoginUserID == "" { + session = session.And("status = ? ", entity.AnswerStatusAvailable) + } else { + session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID) + } + } + + session = session.Limit(search.PageSize, offset) + count, err = session.FindAndCount(&rows) + if err != nil { + return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range rows { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return rows, count, nil +} + +// GetPersonalAnswerPage personal answer page +func (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) ( + resp []*entity.Answer, total int64, err error) { + cond := &entity.Answer{ + UserID: req.UserID, + } + session := ar.data.DB.Context(ctx) + switch req.Order { + case entity.AnswerSearchOrderByTime: + session = session.OrderBy("created_at desc") + case entity.AnswerSearchOrderByTimeAsc: + session = session.OrderBy("created_at asc") + case entity.AnswerSearchOrderByVote: + session = session.OrderBy("vote_count desc") + default: + session = session.OrderBy("adopted desc,vote_count desc,created_at asc") + } + if req.ShowPending { + session = session.And("status != ?", entity.AnswerStatusDeleted) + } else { + session = session.And("status = ?", entity.AnswerStatusAvailable) + } + resp = make([]*entity.Answer, 0) + total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) + if err != nil { + return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range resp { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return resp, total, nil +} + +func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp []*entity.Answer, total int64, err error) { + cond := &entity.Answer{} + session := ar.data.DB.Context(ctx) + if len(req.QuestionID) == 0 && len(req.AnswerID) == 0 { + session.Join("INNER", "question", "answer.question_id = question.id") + if len(req.QuestionTitle) > 0 { + session.Where("question.title like ?", "%"+req.QuestionTitle+"%") + } + } + if len(req.AnswerID) > 0 { + cond.ID = req.AnswerID + } + if len(req.QuestionID) > 0 { + session.Where("answer.question_id = ?", req.QuestionID) + } + if req.Status > 0 { + cond.Status = req.Status + } + session.Desc("answer.created_at") + + resp = make([]*entity.Answer, 0) + total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session) + if err != nil { + return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return resp, total, nil +} + +// SumVotesByQuestionID sum votes by question id +func (ar *answerRepo) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) { + questionID = uid.DeShortID(questionID) + var resp entity.Answer + count, err := ar.data.DB.Context(ctx).Where("question_id = ? and status = ?", questionID, entity.AnswerStatusAvailable).Sum(&resp, "vote_count") + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +// updateSearch update search, if search plugin not enable, do nothing +func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) { + answerID = uid.DeShortID(answerID) + // check search plugin + var ( + s plugin.Search + ) + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + answer, exist, err := ar.GetAnswer(ctx, answerID) + if !exist { + return + } + if err != nil { + return err + } + + // get question + var ( + question = new(entity.Question) + ) + exist, err = ar.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(&question) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + st := ar.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)) + st.Where("status = ?", entity.TagRelStatusAvailable) + err = st.Find(&tagListList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + + content := &plugin.SearchContent{ + ObjectID: answerID, + Title: question.Title, + Type: constant.AnswerObjectType, + Content: answer.OriginalText, + Answers: 0, + Status: plugin.SearchContentStatus(answer.Status), + Tags: tags, + QuestionID: answer.QuestionID, + UserID: answer.UserID, + Views: int64(question.ViewCount), + Created: answer.CreatedAt.Unix(), + Active: answer.UpdatedAt.Unix(), + Score: int64(answer.VoteCount), + HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, + } + err = s.UpdateContent(ctx, content) + return +} + +func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error { + // get all deleted answers ids + ids := make([]string, 0) + err := ar.data.DB.Context(ctx).Select("id").Table(new(entity.Answer).TableName()). + Where("status = ?", entity.AnswerStatusDeleted).Find(&ids) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if len(ids) == 0 { + return nil + } + + // delete all revisions permanently + _, err = ar.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + _, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} diff --git a/internal/repo/answer_repo.go b/internal/repo/answer_repo.go deleted file mode 100644 index dd4753e87..000000000 --- a/internal/repo/answer_repo.go +++ /dev/null @@ -1,229 +0,0 @@ -package repo - -import ( - "context" - "time" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/internal/service/unique" - "github.com/segmentfault/pacman/errors" -) - -// answerRepo answer repository -type answerRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - userRankRepo rank.UserRankRepo - activityRepo activity_common.ActivityRepo -} - -// NewAnswerRepo new repository -func NewAnswerRepo( - data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, - userRankRepo rank.UserRankRepo, - activityRepo activity_common.ActivityRepo, -) answercommon.AnswerRepo { - return &answerRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - userRankRepo: userRankRepo, - activityRepo: activityRepo, - } -} - -// AddAnswer add answer -func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) { - ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName()) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - answer.ID = ID - _, err = ar.data.DB.Insert(answer) - - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -// RemoveAnswer delete answer -func (ar *answerRepo) RemoveAnswer(ctx context.Context, id string) (err error) { - answer := &entity.Answer{ - ID: id, - Status: entity.AnswerStatusDeleted, - } - _, err = ar.data.DB.Where("id = ?", id).Cols("status").Update(answer) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -// UpdateAnswer update answer -func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, Colar []string) (err error) { - _, err = ar.data.DB.ID(answer.ID).Cols(Colar...).Update(answer) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return err -} - -func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) { - now := time.Now() - answer.UpdatedAt = now - _, err = ar.data.DB.Where("id =?", answer.ID).Cols("status", "updated_at").Update(answer) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetAnswer get answer one -func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( - answer *entity.Answer, exist bool, err error) { - answer = &entity.Answer{} - exist, err = ar.data.DB.ID(id).Get(answer) - if err != nil { - return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetAnswerList get answer list all -func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { - answerList = make([]*entity.Answer, 0) - err = ar.data.DB.Find(answerList, answer) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetAnswerPage get answer page -func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) { - answerList = make([]*entity.Answer, 0) - total, err = pager.Help(page, pageSize, answerList, answer, ar.data.DB.NewSession()) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// UpdateAdopted -// If no answer is selected, the answer id can be 0 -func (ar *answerRepo) UpdateAdopted(ctx context.Context, id string, questionId string) error { - if questionId == "" { - return nil - } - var data entity.Answer - data.ID = id - - data.Adopted = schema.Answer_Adopted_Failed - _, err := ar.data.DB.Where("question_id =?", questionId).Cols("adopted").Update(&data) - if err != nil { - return err - } - if id != "0" { - data.Adopted = schema.Answer_Adopted_Enable - _, err = ar.data.DB.Where("id = ?", id).Cols("adopted").Update(&data) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - } - return nil -} - -// GetByID -func (ar *answerRepo) GetByID(ctx context.Context, id string) (*entity.Answer, bool, error) { - var resp entity.Answer - has, err := ar.data.DB.Where("id =? ", id).Get(&resp) - if err != nil { - return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return &resp, has, nil -} - -func (ar *answerRepo) GetByUserIdQuestionId(ctx context.Context, userId string, questionId string) (*entity.Answer, bool, error) { - var resp entity.Answer - has, err := ar.data.DB.Where("question_id =? and user_id = ?", questionId, userId).Get(&resp) - if err != nil { - return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return &resp, has, nil -} - -// SearchList -func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { - var count int64 - var err error - rows := make([]*entity.Answer, 0) - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 - } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize - } - offset := search.Page * search.PageSize - session := ar.data.DB.Where("") - - if search.QuestionID != "" { - session = session.And("question_id = ?", search.QuestionID) - } - if len(search.UserID) > 0 { - session = session.And("user_id = ?", search.UserID) - } - if search.Order == entity.Answer_Search_OrderBy_Time { - session = session.OrderBy("created_at desc") - } else if search.Order == entity.Answer_Search_OrderBy_Vote { - session = session.OrderBy("vote_count desc") - } else { - session = session.OrderBy("adopted desc,vote_count desc") - } - - session = session.And("status = ?", entity.AnswerStatusAvailable) - - session = session.Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) - if err != nil { - return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return rows, count, nil -} - -func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) { - var count int64 - var err error - if search.Status == 0 { - search.Status = 1 - } - rows := make([]*entity.Answer, 0) - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 - } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize - } - offset := search.Page * search.PageSize - session := ar.data.DB.Where("") - session = session.And("status =?", search.Status) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) - if err != nil { - return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return rows, count, nil -} diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go index 462029804..a1e358f9a 100644 --- a/internal/repo/auth/auth.go +++ b/internal/repo/auth/auth.go @@ -1,93 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package auth import ( "context" "encoding/json" + "github.com/apache/answer/internal/service/auth" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/auth" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) -// authRepo activity repository +// authRepo auth repository type authRepo struct { data *data.Data } +// NewAuthRepo new repository +func NewAuthRepo(data *data.Data) auth.AuthRepo { + return &authRepo{ + data: data, + } +} + +// GetUserCacheInfo get user cache info func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { - userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) + userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !exist { + return nil, nil + } userInfo = &entity.UserCacheInfo{} - err = json.Unmarshal([]byte(userInfoCache), userInfo) + _ = json.Unmarshal([]byte(userInfoCache), userInfo) + return userInfo, nil +} + +// SetUserCacheInfo set user cache info +func (ar *authRepo) SetUserCacheInfo(ctx context.Context, + accessToken, visitToken string, userInfo *entity.UserCacheInfo) (err error) { + userInfo.VisitToken = visitToken + userInfoCache, err := json.Marshal(userInfo) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return err } - return userInfo, nil + err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken, + string(userInfoCache), constant.UserTokenCacheTime) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil { + log.Error(err) + } + if len(visitToken) == 0 { + return nil + } + if err := ar.data.Cache.SetString(ctx, constant.UserVisitTokenCacheKey+visitToken, + accessToken, constant.UserTokenCacheTime); err != nil { + log.Error(err) + } + return nil } -func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { - userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) +// GetUserVisitCacheInfo get user visit cache info +func (ar *authRepo) GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) { + accessToken, exist, err := ar.data.Cache.GetString(ctx, constant.UserVisitTokenCacheKey+visitToken) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - userInfo = &entity.UserCacheInfo{} - err = json.Unmarshal([]byte(userInfoCache), userInfo) + if !exist { + return "", nil + } + return accessToken, nil +} + +// RemoveUserCacheInfo remove user cache info +func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { + err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return userInfo, nil + return nil } -func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { - err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) +// RemoveUserVisitCacheInfo remove visit token cache +func (ar *authRepo) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) { + err = ar.data.Cache.Del(ctx, constant.UserVisitTokenCacheKey+visitToken) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { +// SetUserStatus set user status +func (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err } - err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken, - string(userInfoCache), constant.UserTokenCacheTime) + err = ar.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID, + string(userInfoCache), constant.UserStatusChangedCacheTime) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { - err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) +// GetUserStatus get user status +func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { + userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return nil, nil + } + userInfo = &entity.UserCacheInfo{} + _ = json.Unmarshal([]byte(userInfoCache), userInfo) + return userInfo, nil +} + +// RemoveUserStatus remove user status +func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { + err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { - userInfoCache, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken) +// GetAdminUserCacheInfo get admin user cache info +func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { + userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } - userInfo = &entity.UserCacheInfo{} - err = json.Unmarshal([]byte(userInfoCache), userInfo) - if err != nil { - return nil, err + if !exist { + return nil, nil } + userInfo = &entity.UserCacheInfo{} + _ = json.Unmarshal([]byte(userInfoCache), userInfo) return userInfo, nil } -func (ar *authRepo) SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { +// SetAdminUserCacheInfo set admin user cache info +func (ar *authRepo) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err @@ -96,26 +176,63 @@ func (ar *authRepo) SetCmsUserCacheInfo(ctx context.Context, accessToken string, err = ar.data.Cache.SetString(ctx, constant.AdminTokenCacheKey+accessToken, string(userInfoCache), constant.AdminTokenCacheTime) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) { +// RemoveAdminUserCacheInfo remove admin user cache info +func (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -// NewAuthRepo new repository -func NewAuthRepo( - data *data.Data, -) auth.AuthRepo { - return &authRepo{ - data: data, +// AddUserTokenMapping add user token mapping +func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) { + key := constant.UserTokenMappingCacheKey + userID + resp, _, err := ar.data.Cache.GetString(ctx, key) + if err != nil { + return err + } + mapping := make(map[string]bool, 0) + if len(resp) > 0 { + _ = json.Unmarshal([]byte(resp), &mapping) + } + mapping[accessToken] = true + content, _ := json.Marshal(mapping) + return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime) +} + +// RemoveUserTokens Log out all users under this user id +func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) { + key := constant.UserTokenMappingCacheKey + userID + resp, _, err := ar.data.Cache.GetString(ctx, key) + if err != nil { + return + } + mapping := make(map[string]bool, 0) + if len(resp) > 0 { + _ = json.Unmarshal([]byte(resp), &mapping) + log.Debugf("find %d user tokens by user id %s", len(mapping), userID) + } + + for token := range mapping { + if token == remainToken { + continue + } + if err := ar.RemoveUserCacheInfo(ctx, token); err != nil { + log.Error(err) + } else { + log.Debugf("del user %s token success", userID) + } + } + if err := ar.RemoveUserStatus(ctx, userID); err != nil { + log.Error(err) + } + if err := ar.data.Cache.Del(ctx, key); err != nil { + log.Error(err) } } diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go new file mode 100644 index 000000000..8c4656db3 --- /dev/null +++ b/internal/repo/badge/badge_event_rule.go @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/badge" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "strconv" +) + +// eventRuleRepo event rule repo +type eventRuleRepo struct { + data *data.Data + EventRuleMapping map[constant.EventType][]badge.EventRuleHandler +} + +// NewEventRuleRepo creates a new badge repository +func NewEventRuleRepo(data *data.Data) badge.EventRuleRepo { + b := &eventRuleRepo{ + data: data, + } + b.EventRuleMapping = map[constant.EventType][]badge.EventRuleHandler{ + constant.EventUserUpdate: {b.FirstUpdateUserProfile}, + constant.EventUserShare: {b.FirstSharedPost}, + constant.EventQuestionCreate: nil, + constant.EventQuestionUpdate: {b.FirstPostEdit}, + constant.EventQuestionDelete: nil, + constant.EventQuestionVote: {b.FirstVotedPost, b.ReachQuestionVote}, + constant.EventQuestionAccept: {b.FirstAcceptAnswer, b.ReachAnswerAcceptedAmount}, + constant.EventQuestionFlag: {b.FirstFlaggedPost}, + constant.EventQuestionReact: {b.FirstReactedPost}, + constant.EventAnswerCreate: nil, + constant.EventAnswerUpdate: {b.FirstPostEdit}, + constant.EventAnswerDelete: nil, + constant.EventAnswerVote: {b.FirstVotedPost, b.ReachAnswerVote}, + constant.EventAnswerFlag: {b.FirstFlaggedPost}, + constant.EventAnswerReact: {b.FirstReactedPost}, + constant.EventCommentCreate: nil, + constant.EventCommentUpdate: nil, + constant.EventCommentDelete: nil, + constant.EventCommentVote: {b.FirstVotedPost}, + constant.EventCommentFlag: {b.FirstFlaggedPost}, + } + return b +} + +// HandleEventWithRule handle event with rule +func (br *eventRuleRepo) HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) ( + awards []*entity.BadgeAward) { + handlers := br.EventRuleMapping[msg.EventType] + for _, handler := range handlers { + t, err := handler(ctx, msg) + if err != nil { + log.Errorf("error handling badge event %+v: %v", msg, err) + } else { + awards = append(awards, t...) + } + } + return awards +} + +// FirstUpdateUserProfile first update user profile +func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstUpdateUserProfile") + for _, b := range badges { + bean := &entity.User{ID: event.UserID} + exist, err := br.data.DB.Context(ctx).Get(bean) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + continue + } + if len(bean.Bio) > 0 { + awards = append(awards, br.createBadgeAward(event.UserID, entity.BadgeEmptyAwardKey, b)) + } + } + return awards, nil +} + +// FirstPostEdit first post edit +func (br *eventRuleRepo) FirstPostEdit(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstPostEdit") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// FirstFlaggedPost first flagged post. +func (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstFlaggedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// FirstVotedPost first voted post +func (br *eventRuleRepo) FirstVotedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstVotedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// FirstReactedPost first reacted post +func (br *eventRuleRepo) FirstReactedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstReactedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// FirstSharedPost first shared post +func (br *eventRuleRepo) FirstSharedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstSharedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// FirstAcceptAnswer user first accept answer +func (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "FirstAcceptAnswer") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) + } + return awards, nil +} + +// ReachAnswerAcceptedAmount reach answer accepted amount +func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "ReachAnswerAcceptedAmount") + if len(event.AnswerUserID) == 0 { + return nil, nil + } + + // count user's accepted answer amount + amount, err := br.data.DB.Context(ctx).Count(&entity.Answer{ + UserID: event.AnswerUserID, + Accepted: schema.AnswerAcceptedEnable, + Status: entity.AnswerStatusAvailable, + }) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || amount < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) + } + return awards, nil +} + +// ReachAnswerVote reach answer vote +func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "ReachAnswerVote") + // get vote amount + amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) + if amount == 0 { + return nil, nil + } + + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) + } + return awards, nil +} + +// ReachQuestionVote reach question vote +func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + badges := br.getBadgesByHandler(ctx, "ReachQuestionVote") + // get vote amount + amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) + if amount == 0 { + return nil, nil + } + + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.QuestionUserID, event.QuestionID, b)) + } + return awards, nil +} + +func (br *eventRuleRepo) getBadgesByHandler(ctx context.Context, handler string) (badges []*entity.Badge) { + badges = make([]*entity.Badge, 0) + err := br.data.DB.Context(ctx).Where("handler = ?", handler).Find(&badges) + if err != nil { + log.Errorf("error getting badge by handler %s: %v", handler, err) + return nil + } + return badges +} + +func (br *eventRuleRepo) createBadgeAward(userID, awardKey string, badge *entity.Badge) (awards *entity.BadgeAward) { + return &entity.BadgeAward{ + UserID: userID, + BadgeID: badge.ID, + AwardKey: awardKey, + } +} diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go new file mode 100644 index 000000000..257caef81 --- /dev/null +++ b/internal/repo/badge/badge_repo.go @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" +) + +type badgeRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +// NewBadgeRepo creates a new badge repository +func NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeRepo { + return &badgeRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +func (r *badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) { + badge = &entity.Badge{} + exists, err = r.data.DB.Context(ctx).Where("id = ?", id).Get(badge) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (r *badgeRepo) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).In("id", ids).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// ListPaged returns a list of activated badges +func (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { + badges = make([]*entity.Badge, 0) + total = 0 + + session := r.data.DB.Context(ctx).Where("status <> ?", entity.BadgeStatusDeleted) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// ListActivated returns a list of activated badges +func (r *badgeRepo) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { + badges = make([]*entity.Badge, 0) + total = 0 + + session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// ListInactivated returns a list of inactivated badges +func (r *badgeRepo) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { + badges = make([]*entity.Badge, 0) + total = 0 + + session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateStatus updates the award count of a badge +func (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (err error) { + _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + _, err = session.ID(id).Update(&entity.Badge{ + Status: status, + }) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(session.Rollback()).WithStack() + return + } + if status >= entity.BadgeStatusDeleted { + _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ + IsBadgeDeleted: entity.IsBadgeDeleted, + }) + } else { + _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ + IsBadgeDeleted: entity.IsBadgeNotDeleted, + }) + } + return + }) + + return +} + +// UpdateAwardCount updates the award count of a badge +func (r *badgeRepo) UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) { + _, err = r.data.DB.Context(ctx).ID(badgeID).Cols("award_count").Update(&entity.Badge{AwardCount: awardCount}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go new file mode 100644 index 000000000..eda5d80c2 --- /dev/null +++ b/internal/repo/badge_award/badge_award_repo.go @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_award + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" +) + +type badgeAwardRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeAwardRepo { + return &badgeAwardRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +// AwardBadgeForUser award badge for user +func (r *badgeAwardRepo) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) { + badgeAward.ID, err = r.uniqueIDRepo.GenUniqueIDStr(ctx, entity.BadgeAward{}.TableName()) + if err != nil { + return err + } + + _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + badgeInfo := &entity.Badge{} + exist, err := session.ID(badgeAward.BadgeID).ForUpdate().Get(badgeInfo) + if err != nil { + return nil, err + } + if !exist { + return nil, fmt.Errorf("badge not exist") + } + + old := &entity.BadgeAward{ + UserID: badgeAward.UserID, + BadgeID: badgeAward.BadgeID, + IsBadgeDeleted: entity.IsBadgeNotDeleted, + } + if badgeInfo.Single != entity.BadgeSingleAward { + old.AwardKey = badgeAward.AwardKey + } + exist, err = session.Get(old) + if err != nil { + return nil, err + } + if exist { + return nil, fmt.Errorf("badge already awarded") + } + + _, err = session.Insert(badgeAward) + if err != nil { + return nil, err + } + + return session.ID(badgeInfo.ID).Incr("award_count", 1).Update(&entity.Badge{}) + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +// CheckIsAward check this badge is awarded for this user or not +func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awardKey string, singleOrMulti int8) ( + isAward bool, err error) { + if singleOrMulti == entity.BadgeSingleAward { + _, isAward, err = r.GetByUserIdAndBadgeId(ctx, userID, badgeID) + } else { + _, isAward, err = r.GetByUserIdAndBadgeIdAndAwardKey(ctx, userID, badgeID, awardKey) + } + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return isAward, err +} + +func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) { + awardCount, err := r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&entity.BadgeAward{}) + if err != nil { + return 0 + } + return +} + +func (r *badgeAwardRepo) CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) { + awardCount, err = r.data.DB.Context(ctx).Count(&entity.BadgeAward{BadgeID: badgeID}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) { + err = r.data.DB.Context(ctx).Select("badge_id, count(`id`) AS earned_count").Where("user_id = ?", userID).GroupBy("badge_id").Find(&earnedCounts) + return +} + +// ListPagedByBadgeId list badge awards by badge id +func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { + session := r.data.DB.Context(ctx) + session.Where("badge_id = ?", badgeID) + total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.BadgeAward{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// ListPagedByBadgeIdAndUserId list badge awards by badge id and user id +func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { + session := r.data.DB.Context(ctx) + session.Where("badge_id = ? AND user_id = ?", badgeID, userID) + total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// ListNewestEarned list newest earned badge awards +func (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) { + badgeAwards = make([]*entity.BadgeAwardRecent, 0) + err = r.data.DB.Context(ctx). + Select("badge_id, max(created_at) created,count(*) earned_count"). + Where("user_id = ? AND is_badge_deleted = ? ", userID, entity.IsBadgeNotDeleted). + GroupBy("badge_id"). + OrderBy("created desc"). + Limit(limit).Find(&badgeAwards) + return +} + +// GetByUserIdAndBadgeId get badge award by user id and badge id +func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) ( + badgeAward *entity.BadgeAward, exists bool, err error) { + badgeAward = &entity.BadgeAward{} + exists, err = r.data.DB.Context(ctx). + Where("user_id = ? AND badge_id = ? AND is_badge_deleted = 0", userID, badgeID).Get(badgeAward) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetByUserIdAndBadgeIdAndAwardKey get badge award by user id and badge id and award key +func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) ( + badgeAward *entity.BadgeAward, exists bool, err error) { + badgeAward = &entity.BadgeAward{} + exists, err = r.data.DB.Context(ctx). + Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", userID, badgeID, awardKey).Get(badgeAward) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// DeleteUserBadgeAward delete user badge award +func (r *badgeAwardRepo) DeleteUserBadgeAward(ctx context.Context, userID string) (err error) { + _, err = r.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.BadgeAward{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/badge_group/badge_group_repo.go b/internal/repo/badge_group/badge_group_repo.go new file mode 100644 index 000000000..839ba4691 --- /dev/null +++ b/internal/repo/badge_group/badge_group_repo.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_group + +import ( + "context" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/internal/service/unique" +) + +type badgeGroupRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeGroupRepo { + return &badgeGroupRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +func (r *badgeGroupRepo) ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) { + groups = make([]*entity.BadgeGroup, 0) + err = r.data.DB.Context(ctx).Find(&groups) + return +} + +func (r *badgeGroupRepo) AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) { + return +} diff --git a/internal/repo/captcha/captcha.go b/internal/repo/captcha/captcha.go index 73ca14502..f8195c1ae 100644 --- a/internal/repo/captcha/captcha.go +++ b/internal/repo/captcha/captcha.go @@ -1,14 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package captcha import ( "context" + "encoding/json" "fmt" "time" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/service/action" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/action" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) // captchaRepo captcha repository @@ -23,27 +45,41 @@ func NewCaptchaRepo(data *data.Data) action.CaptchaRepo { } } -func (cr *captchaRepo) SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType) - err = cr.data.Cache.SetInt64(ctx, cacheKey, int64(amount), 6*time.Minute) +func (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) + value := &entity.ActionRecordInfo{} + value.LastTime = now.Unix() + value.Num = amount + valueStr, err := json.Marshal(value) + if err != nil { + return nil + } + err = cr.data.Cache.SetString(ctx, cacheKey, string(valueStr), 6*time.Minute) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -func (cr *captchaRepo) GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType) - res, err := cr.data.Cache.GetInt64(ctx, cacheKey) +func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actionInfo *entity.ActionRecordInfo, err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) + res, exist, err := cr.data.Cache.GetString(ctx, cacheKey) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err + } + if !exist { + return nil, nil } - // TODO: cache reflect should return empty when key not found - return int(res), nil + actionInfo = &entity.ActionRecordInfo{} + _ = json.Unmarshal([]byte(res), actionInfo) + return actionInfo, nil } -func (cr *captchaRepo) DelActionType(ctx context.Context, ip, actionType string) (err error) { - cacheKey := fmt.Sprintf("ActionRecord:%s@%s", ip, actionType) +func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) { + now := time.Now() + cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02")) err = cr.data.Cache.Del(ctx, cacheKey) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -53,7 +89,6 @@ func (cr *captchaRepo) DelActionType(ctx context.Context, ip, actionType string) // SetCaptcha set captcha to cache func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err error) { - // TODO make cache time to config err = cr.data.Cache.SetString(ctx, key, captcha, 6*time.Minute) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -63,10 +98,20 @@ func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err // GetCaptcha get captcha from cache func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) { - captcha, err = cr.data.Cache.GetString(ctx, key) + captcha, exist, err := cr.data.Cache.GetString(ctx, key) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return "", err + } + if !exist { + return "", fmt.Errorf("captcha not exist") } - // TODO: cache reflect should return empty when key not found return captcha, nil } + +func (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) { + err = cr.data.Cache.Del(ctx, key) + if err != nil { + log.Debug(err) + } + return nil +} diff --git a/internal/repo/collection/collection_group_repo.go b/internal/repo/collection/collection_group_repo.go index cac96cfcf..f8dc9c209 100644 --- a/internal/repo/collection/collection_group_repo.go +++ b/internal/repo/collection/collection_group_repo.go @@ -1,14 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package collection import ( "context" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/apache/answer/internal/service/collection" + "xorm.io/xorm" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/errors" ) @@ -18,7 +39,7 @@ type collectionGroupRepo struct { } // NewCollectionGroupRepo new repository -func NewCollectionGroupRepo(data *data.Data) service.CollectionGroupRepo { +func NewCollectionGroupRepo(data *data.Data) collection.CollectionGroupRepo { return &collectionGroupRepo{ data: data, } @@ -26,7 +47,7 @@ func NewCollectionGroupRepo(data *data.Data) service.CollectionGroupRepo { // AddCollectionGroup add collection group func (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) { - _, err = cr.data.DB.Insert(collectionGroup) + _, err = cr.data.DB.Context(ctx).Insert(collectionGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -37,10 +58,10 @@ func (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectio func (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) { defaultGroup := &entity.CollectionGroup{ Name: "default", - DefaultGroup: schema.CG_DEFAULT, + DefaultGroup: schema.CGDefault, UserID: userID, } - _, err = cr.data.DB.Insert(defaultGroup) + _, err = cr.data.DB.Context(ctx).Insert(defaultGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return @@ -49,9 +70,45 @@ func (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, us return } +// CreateDefaultGroupIfNotExist create default group if not exist +func (cr *collectionGroupRepo) CreateDefaultGroupIfNotExist(ctx context.Context, userID string) ( + collectionGroup *entity.CollectionGroup, err error) { + _, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + old := &entity.CollectionGroup{ + UserID: userID, + DefaultGroup: schema.CGDefault, + } + exist, err := session.ForUpdate().Get(old) + if err != nil { + return nil, err + } + if exist { + collectionGroup = old + return old, nil + } + + defaultGroup := &entity.CollectionGroup{ + Name: "default", + DefaultGroup: schema.CGDefault, + UserID: userID, + } + _, err = session.Insert(defaultGroup) + if err != nil { + return nil, err + } + collectionGroup = defaultGroup + return nil, nil + }) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return collectionGroup, nil +} + // UpdateCollectionGroup update collection group func (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) { - _, err = cr.data.DB.ID(collectionGroup.ID).Cols(cols...).Update(collectionGroup) + _, err = cr.data.DB.Context(ctx).ID(collectionGroup.ID).Cols(cols...).Update(collectionGroup) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -60,9 +117,10 @@ func (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collec // GetCollectionGroup get collection group one func (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string) ( - collectionGroup *entity.CollectionGroup, exist bool, err error) { + collectionGroup *entity.CollectionGroup, exist bool, err error, +) { collectionGroup = &entity.CollectionGroup{} - exist, err = cr.data.DB.ID(id).Get(collectionGroup) + exist, err = cr.data.DB.Context(ctx).ID(id).Get(collectionGroup) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -73,7 +131,7 @@ func (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string func (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) { collectionGroupList = make([]*entity.CollectionGroup, 0) - session := cr.data.DB.NewSession() + session := cr.data.DB.Context(ctx) if collectionGroup.UserID != "" && collectionGroup.UserID != "0" { session = session.Where("user_id = ?", collectionGroup.UserID) } @@ -84,9 +142,9 @@ func (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, return } -func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userId string) (collectionGroup *entity.CollectionGroup, has bool, err error) { +func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) { collectionGroup = &entity.CollectionGroup{} - has, err = cr.data.DB.Where("user_id =? and default_group = ?", userId, schema.CG_DEFAULT).Get(collectionGroup) + has, err = cr.data.DB.Context(ctx).Where("user_id =? and default_group = ?", userID, schema.CGDefault).Get(collectionGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return diff --git a/internal/repo/collection/collection_repo.go b/internal/repo/collection/collection_repo.go index 2fb4d92cf..a3faacdb5 100644 --- a/internal/repo/collection/collection_repo.go +++ b/internal/repo/collection/collection_repo.go @@ -1,16 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package collection import ( "context" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" ) // collectionRepo collection repository @@ -29,20 +50,39 @@ func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collec // AddCollection add collection func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) { - id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName()) - if err == nil { - collection.ID = id - _, err = cr.data.DB.Insert(collection) + collection.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName()) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + _, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + old := &entity.Collection{ + UserID: collection.UserID, + ObjectID: collection.ObjectID, + } + exist, err := session.ForUpdate().Get(old) + if err != nil { + return nil, err + } + if exist { + return nil, nil + } + _, err = session.Insert(collection) if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, err } + return + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // RemoveCollection delete collection func (cr *collectionRepo) RemoveCollection(ctx context.Context, id string) (err error) { - _, err = cr.data.DB.Where("id =?", id).Delete(&entity.Collection{}) + _, err = cr.data.DB.Context(ctx).Where("id = ?", id).Delete(&entity.Collection{}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -51,14 +91,14 @@ func (cr *collectionRepo) RemoveCollection(ctx context.Context, id string) (err // UpdateCollection update collection func (cr *collectionRepo) UpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error) { - _, err = cr.data.DB.ID(collection.ID).Cols(cols...).Update(collection) + _, err = cr.data.DB.Context(ctx).ID(collection.ID).Cols(cols...).Update(collection) return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } // GetCollection get collection one func (cr *collectionRepo) GetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error) { collection = &entity.Collection{} - exist, err = cr.data.DB.ID(id).Get(collection) + exist, err = cr.data.DB.Context(ctx).ID(id).Get(collection) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -68,15 +108,15 @@ func (cr *collectionRepo) GetCollection(ctx context.Context, id int) (collection // GetCollectionList get collection list all func (cr *collectionRepo) GetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error) { collectionList = make([]*entity.Collection, 0) - err = cr.data.DB.Find(collectionList, collection) + err = cr.data.DB.Context(ctx).Find(&collectionList, collection) err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return } // GetOneByObjectIDAndUser get one by object TagID and user -func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userId string, objectId string) (collection *entity.Collection, exist bool, err error) { +func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userID string, objectID string) (collection *entity.Collection, exist bool, err error) { collection = &entity.Collection{} - exist, err = cr.data.DB.Where("user_id = ? and object_id = ?", userId, objectId).Get(collection) + exist, err = cr.data.DB.Context(ctx).Where("user_id = ? and object_id = ?", userID, objectID).Get(collection) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -84,9 +124,9 @@ func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userId st } // SearchByObjectIDsAndUser search by object IDs and user -func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userId string, objectIds []string) ([]*entity.Collection, error) { +func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userID string, objectIDs []string) ([]*entity.Collection, error) { collectionList := make([]*entity.Collection, 0) - err := cr.data.DB.Where("user_id = ?", userId).In("object_id", objectIds).Find(&collectionList) + err := cr.data.DB.Context(ctx).Where("user_id = ?", userID).In("object_id", objectIDs).Find(&collectionList) if err != nil { return collectionList, err } @@ -94,9 +134,9 @@ func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userId s } // CountByObjectID count by object TagID -func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectId string) (total int64, err error) { +func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectID string) (total int64, err error) { collection := &entity.Collection{} - total, err = cr.data.DB.Where("object_id = ?", objectId).Count(collection) + total, err = cr.data.DB.Context(ctx).Where("object_id = ?", objectID).Count(collection) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -105,10 +145,9 @@ func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectId string) // GetCollectionPage get collection page func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) { - collectionList = make([]*entity.Collection, 0) - session := cr.data.DB.NewSession() + session := cr.data.DB.Context(ctx) if collection.UserID != "" && collection.UserID != "0" { session = session.Where("user_id = ?", collection.UserID) } @@ -119,22 +158,32 @@ func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize session = session.OrderBy("update_time desc") total, err = pager.Help(page, pageSize, collectionList, collection, session) - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } // SearchObjectCollected check object is collected or not -func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userId string, objectIds []string) (map[string]bool, error) { - collectedMap := make(map[string]bool) - list, err := cr.SearchByObjectIDsAndUser(ctx, userId, objectIds) +func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) { + for i := 0; i < len(objectIds); i++ { + objectIds[i] = uid.DeShortID(objectIds[i]) + } + + list, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return collectedMap, err + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + + collectedMap := make(map[string]bool) + short := handler.GetEnableShortID(ctx) for _, item := range list { + if short { + item.ObjectID = uid.EnShortID(item.ObjectID) + } collectedMap[item.ObjectID] = true } - return collectedMap, err + return collectedMap, nil } // SearchList @@ -148,10 +197,10 @@ func (cr *collectionRepo) SearchList(ctx context.Context, search *entity.Collect search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize - session := cr.data.DB.Where("") + session := cr.data.DB.Context(ctx).Where("") if len(search.UserID) > 0 { session = session.And("user_id = ?", search.UserID) } else { diff --git a/internal/repo/comment/comment_repo.go b/internal/repo/comment/comment_repo.go index f2da8db59..f1794824c 100644 --- a/internal/repo/comment/comment_repo.go +++ b/internal/repo/comment/comment_repo.go @@ -1,15 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package comment import ( "context" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/comment" - "github.com/answerdev/answer/internal/service/comment_common" - "github.com/answerdev/answer/internal/service/unique" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) @@ -41,7 +62,7 @@ func (cr *commentRepo) AddComment(ctx context.Context, comment *entity.Comment) if err != nil { return err } - _, err = cr.data.DB.Insert(comment) + _, err = cr.data.DB.Context(ctx).Insert(comment) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -50,7 +71,7 @@ func (cr *commentRepo) AddComment(ctx context.Context, comment *entity.Comment) // RemoveComment delete comment func (cr *commentRepo) RemoveComment(ctx context.Context, commentID string) (err error) { - session := cr.data.DB.ID(commentID) + session := cr.data.DB.Context(ctx).ID(commentID) _, err = session.Update(&entity.Comment{Status: entity.CommentStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -58,9 +79,13 @@ func (cr *commentRepo) RemoveComment(ctx context.Context, commentID string) (err return } -// UpdateComment update comment -func (cr *commentRepo) UpdateComment(ctx context.Context, comment *entity.Comment) (err error) { - _, err = cr.data.DB.ID(comment.ID).Where("user_id = ?", comment.UserID).Update(comment) +// UpdateCommentContent update comment +func (cr *commentRepo) UpdateCommentContent( + ctx context.Context, commentID string, originalText string, parsedText string) (err error) { + _, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{ + OriginalText: originalText, + ParsedText: parsedText, + }) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -71,19 +96,40 @@ func (cr *commentRepo) UpdateComment(ctx context.Context, comment *entity.Commen func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( comment *entity.Comment, exist bool, err error) { comment = &entity.Comment{} - exist, err = cr.data.DB.ID(commentID).Get(comment) + exist, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).ID(commentID).Get(comment) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetCommentWithoutStatus get comment one without status +func (cr *commentRepo) GetCommentWithoutStatus(ctx context.Context, commentID string) ( + comment *entity.Comment, exist bool, err error) { + comment = &entity.Comment{} + exist, err = cr.data.DB.Context(ctx).ID(commentID).Get(comment) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } +func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Comment, 0) + count, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetCommentPage get comment page func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( - commentList []*entity.Comment, total int64, err error) { + commentList []*entity.Comment, total int64, err error, +) { commentList = make([]*entity.Comment, 0) - session := cr.data.DB.NewSession() + session := cr.data.DB.Context(ctx) session.OrderBy(commentQuery.GetOrderBy()) session.Where("status = ?", entity.CommentStatusAvailable) @@ -94,3 +140,15 @@ func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment } return } + +// RemoveAllUserComment remove all user comment +func (cr *commentRepo) RemoveAllUserComment(ctx context.Context, userID string) (err error) { + session := cr.data.DB.Context(ctx).Where("user_id = ?", userID) + session.Where("status != ?", entity.CommentStatusDeleted) + affected, err := session.Update(&entity.Comment{Status: entity.CommentStatusDeleted}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + log.Infof("delete user comment, userID: %s, affected: %d", userID, affected) + return +} diff --git a/internal/repo/common/common.go b/internal/repo/common/common.go deleted file mode 100644 index 89b154538..000000000 --- a/internal/repo/common/common.go +++ /dev/null @@ -1,95 +0,0 @@ -package common - -import ( - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/unique" - "github.com/answerdev/answer/pkg/obj" - "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" -) - -type CommonRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo -} - -func NewCommonRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) *CommonRepo { - return &CommonRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - } -} - -// GetRootObjectID get root object ID -func (cr *CommonRepo) GetRootObjectID(objectID string) (rootObjectID string, err error) { - var ( - exist bool - objectType string - answer = entity.Answer{} - comment = entity.Comment{} - ) - - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) - switch objectType { - case "answer": - exist, err = cr.data.DB.ID(objectID).Get(&answer) - if !exist { - err = errors.BadRequest(reason.ObjectNotFound) - } else { - objectID = answer.QuestionID - } - case "comment": - exist, err = cr.data.DB.ID(objectID).Get(&comment) - if !exist { - err = errors.BadRequest(reason.ObjectNotFound) - } else { - objectID, err = cr.GetRootObjectID(comment.ObjectID) - } - default: - rootObjectID = objectID - } - return -} - -// GetObjectIDMap get object ID map from object id -func (cr *CommonRepo) GetObjectIDMap(objectID string) (objectIDMap map[string]string, err error) { - var ( - exist bool - ID, - objectType string - answer = entity.Answer{} - comment = entity.Comment{} - ) - - objectIDMap = map[string]string{} - // 10070000000000450 - objectType, err = obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - log.Error("get report object type:", objectID, ",err:", err) - return - } - switch objectType { - case "answer": - exist, err = cr.data.DB.ID(objectID).Get(&answer) - if !exist { - err = errors.BadRequest(reason.ObjectNotFound) - } else { - objectIDMap, err = cr.GetObjectIDMap(answer.QuestionID) - ID = answer.ID - } - case "comment": - exist, err = cr.data.DB.ID(objectID).Get(&comment) - if !exist { - err = errors.BadRequest(reason.ObjectNotFound) - } else { - objectIDMap, err = cr.GetObjectIDMap(comment.ObjectID) - ID = comment.ID - } - case "question": - ID = objectID - } - objectIDMap[objectType] = ID - return -} diff --git a/internal/repo/config/config_repo.go b/internal/repo/config/config_repo.go index 4f97ad684..3708aa3ec 100644 --- a/internal/repo/config/config_repo.go +++ b/internal/repo/config/config_repo.go @@ -1,29 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package config import ( - "encoding/json" + "context" "fmt" - "sync" - - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/pkg/converter" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/config" "github.com/segmentfault/pacman/errors" -) - -var ( - Key2ValueMapping = make(map[string]interface{}) - Key2IDMapping = make(map[string]int) - ID2KeyMapping = make(map[int]string) + "github.com/segmentfault/pacman/log" ) // configRepo config repository type configRepo struct { data *data.Data - mu sync.Mutex } // NewConfigRepo new repository @@ -31,103 +42,90 @@ func NewConfigRepo(data *data.Data) config.ConfigRepo { repo := &configRepo{ data: data, } - repo.init() return repo } -// init initializes the Key2ValueMapping map data structures -func (cr *configRepo) init() { - cr.mu.Lock() - defer cr.mu.Unlock() - rows := &[]entity.Config{} - err := cr.data.DB.Find(rows) - if err == nil { - for _, row := range *rows { - Key2ValueMapping[row.Key] = row.Value - Key2IDMapping[row.Key] = row.ID - ID2KeyMapping[row.ID] = row.Key +func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) { + cacheKey := fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, id) + cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey) + if err == nil && exist && len(cacheData) > 0 { + c = &entity.Config{} + c.BuildByJSON([]byte(cacheData)) + if c.ID > 0 { + return c, nil } } -} -// Get Base method for getting the config value -// Key string -func (cr *configRepo) Get(key string) (interface{}, error) { - value, ok := Key2ValueMapping[key] - if ok { - return value, nil - } else { - return value, errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config key: %v", key)) + c = &entity.Config{} + exist, err = cr.data.DB.Context(ctx).ID(id).Get(c) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return nil, fmt.Errorf("config not found by id: %d", id) } -} -// GetString method for getting the config value to string -// key string -func (cr *configRepo) GetString(key string) (string, error) { - value, err := cr.Get(key) - if value != nil { - return value.(string), err + // update cache + if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil { + log.Error(err) } - return "", err + return c, nil } -// GetInt method for getting the config value to int64 -// key string -func (cr *configRepo) GetInt(key string) (int, error) { - value, err := cr.GetString(key) - if err != nil { - return 0, err - } else { - return converter.StringToInt(value), nil +func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) { + cacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key + cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey) + if err == nil && exist && len(cacheData) > 0 { + c = &entity.Config{} + c.BuildByJSON([]byte(cacheData)) + if c.ID > 0 { + return c, nil + } } -} -// GetArrayString method for getting the config value to string array -func (cr *configRepo) GetArrayString(key string) ([]string, error) { - arr := &[]string{} - value, err := cr.GetString(key) + c = &entity.Config{Key: key} + exist, err = cr.data.DB.Context(ctx).Get(c) if err != nil { - return nil, err + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return nil, fmt.Errorf("config not found by key: %s", key) } - err = json.Unmarshal([]byte(value), arr) - return *arr, err -} -// GetConfigType method for getting the config type -func (cr *configRepo) GetConfigType(key string) (int, error) { - value, ok := Key2IDMapping[key] - if ok { - return value, nil - } else { - return 0, errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config type: %v", key)) + // update cache + if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil { + log.Error(err) } + return c, nil } -// GetConfigById get config key from config id -func (cr *configRepo) GetConfigById(id int, value any) (err error) { - var ( - ok = true - key string - conf interface{} - ) - key, ok = ID2KeyMapping[id] - if !ok { - err = errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config id: %v", id)) - return +func (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) { + // check if key exists + oldConfig := &entity.Config{Key: key} + exist, err := cr.data.DB.Context(ctx).Get(oldConfig) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) } - conf, err = cr.Get(key) - value = json.Unmarshal([]byte(conf.(string)), value) - return -} - -func (cr *configRepo) SetConfig(key, value string) (err error) { - id := Key2IDMapping[key] - _, err = cr.data.DB.ID(id).Update(&entity.Config{Value: value}) + // update database + _, err = cr.data.DB.Context(ctx).ID(oldConfig.ID).Update(&entity.Config{Value: value}) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } else { - Key2ValueMapping[key] = value + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + oldConfig.Value = value + cacheVal := oldConfig.JsonString() + // update cache + if err := cr.data.Cache.SetString(ctx, + constant.ConfigKEY2ContentCacheKeyPrefix+key, cacheVal, constant.ConfigCacheTime); err != nil { + log.Error(err) + } + if err := cr.data.Cache.SetString(ctx, + fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, oldConfig.ID), cacheVal, constant.ConfigCacheTime); err != nil { + log.Error(err) } return } diff --git a/internal/repo/export/email_repo.go b/internal/repo/export/email_repo.go index 85f0b2495..1f8e1ce83 100644 --- a/internal/repo/export/email_repo.go +++ b/internal/repo/export/email_repo.go @@ -1,12 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package export import ( "context" + "github.com/apache/answer/internal/base/constant" + "github.com/tidwall/gjson" "time" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/service/export" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/export" "github.com/segmentfault/pacman/errors" ) @@ -22,18 +43,56 @@ func NewEmailRepo(data *data.Data) export.EmailRepo { } } -func (e *emailRepo) SetCode(ctx context.Context, code, content string) error { - err := e.data.Cache.SetString(ctx, code, content, 10*time.Minute) - if err != nil { +// SetCode The email code is used to verify that the link in the message is out of date +func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error { + // Setting the latest code is to help ensure that only one link is active at a time. + // Set userID -> latest code + if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // Set latest code -> content + if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } +// VerifyCode verify the code if out of date func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) { - content, err = e.data.Cache.GetString(ctx, code) + // Get latest code -> content + codeCacheKey := constant.UserEmailCodeCacheKey + code + content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return "", err + } + if !exist { + return "", nil + } + + // Delete the code after verification + _ = e.data.Cache.Del(ctx, codeCacheKey) + + // If some email content does not need to verify the latest code is the same as the code, skip it. + // For example, some unsubscribe email content does not need to verify the latest code. + // This link always works before the code is out of date. + if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode { + return content, nil + } + userID := gjson.Get(content, "user_id").String() + + // Get userID -> latest code + latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID) + if err != nil { + return "", err + } + if !exist { + return "", nil + } + + // Check if the latest code is the same as the code, if not, means the code is out of date + if latestCode != code { + return "", nil } - return + return content, nil } diff --git a/internal/repo/file_record/file_record_repo.go b/internal/repo/file_record/file_record_repo.go new file mode 100644 index 000000000..ce486c7ab --- /dev/null +++ b/internal/repo/file_record/file_record_repo.go @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package file_record + +import ( + "context" + + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/service/file_record" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/errors" +) + +// fileRecordRepo fileRecord repository +type fileRecordRepo struct { + data *data.Data +} + +// NewFileRecordRepo new repository +func NewFileRecordRepo(data *data.Data) file_record.FileRecordRepo { + return &fileRecordRepo{ + data: data, + } +} + +// AddFileRecord add file record +func (fr *fileRecordRepo) AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) { + _, err = fr.data.DB.Context(ctx).Insert(fileRecord) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetFileRecordPage get fileRecord page +func (fr *fileRecordRepo) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) ( + fileRecordList []*entity.FileRecord, total int64, err error) { + fileRecordList = make([]*entity.FileRecord, 0) + + session := fr.data.DB.Context(ctx) + total, err = pager.Help(page, pageSize, &fileRecordList, cond, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// DeleteFileRecord delete file record +func (fr *fileRecordRepo) DeleteFileRecord(ctx context.Context, id int) (err error) { + _, err = fr.data.DB.Context(ctx).ID(id).Cols("status").Update(&entity.FileRecord{Status: entity.FileRecordStatusDeleted}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateFileRecord update file record +func (fr *fileRecordRepo) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) { + _, err = fr.data.DB.Context(ctx).ID(fileRecord.ID).Update(fileRecord) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetFileRecordByURL gets a file record by its url +func (fr *fileRecordRepo) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) { + record = &entity.FileRecord{} + session := fr.data.DB.Context(ctx) + exists, err := session.Where("file_url = ? AND status = ?", fileURL, entity.FileRecordStatusAvailable).Get(record) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if !exists { + return + } + return record, nil +} diff --git a/internal/repo/limit/limit.go b/internal/repo/limit/limit.go new file mode 100644 index 000000000..4868accd5 --- /dev/null +++ b/internal/repo/limit/limit.go @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package limit + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/segmentfault/pacman/errors" + "time" +) + +// LimitRepo auth repository +type LimitRepo struct { + data *data.Data +} + +// NewRateLimitRepo new repository +func NewRateLimitRepo(data *data.Data) *LimitRepo { + return &LimitRepo{ + data: data, + } +} + +// CheckAndRecord check +func (lr *LimitRepo) CheckAndRecord(ctx context.Context, key string) (limit bool, err error) { + _, exist, err := lr.data.Cache.GetString(ctx, constant.RateLimitCacheKeyPrefix+key) + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + return true, nil + } + err = lr.data.Cache.SetString(ctx, constant.RateLimitCacheKeyPrefix+key, + fmt.Sprintf("%d", time.Now().Unix()), constant.RateLimitCacheTime) + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return false, nil +} + +// ClearRecord clear +func (lr *LimitRepo) ClearRecord(ctx context.Context, key string) error { + return lr.data.Cache.Del(ctx, constant.RateLimitCacheKeyPrefix+key) +} diff --git a/internal/repo/meta/meta_repo.go b/internal/repo/meta/meta_repo.go index e98c8020c..767bd04c7 100644 --- a/internal/repo/meta/meta_repo.go +++ b/internal/repo/meta/meta_repo.go @@ -1,15 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package meta import ( "context" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/meta" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/meta_common" "github.com/segmentfault/pacman/errors" "xorm.io/builder" + "xorm.io/xorm" ) // metaRepo meta repository @@ -18,7 +37,7 @@ type metaRepo struct { } // NewMetaRepo new repository -func NewMetaRepo(data *data.Data) meta.MetaRepo { +func NewMetaRepo(data *data.Data) metacommon.MetaRepo { return &metaRepo{ data: data, } @@ -26,7 +45,7 @@ func NewMetaRepo(data *data.Data) meta.MetaRepo { // AddMeta add meta func (mr *metaRepo) AddMeta(ctx context.Context, meta *entity.Meta) (err error) { - _, err = mr.data.DB.Insert(meta) + _, err = mr.data.DB.Context(ctx).Insert(meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -35,7 +54,7 @@ func (mr *metaRepo) AddMeta(ctx context.Context, meta *entity.Meta) (err error) // RemoveMeta delete meta func (mr *metaRepo) RemoveMeta(ctx context.Context, id int) (err error) { - _, err = mr.data.DB.ID(id).Delete(&entity.Meta{}) + _, err = mr.data.DB.Context(ctx).ID(id).Delete(&entity.Meta{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -44,18 +63,47 @@ func (mr *metaRepo) RemoveMeta(ctx context.Context, id int) (err error) { // UpdateMeta update meta func (mr *metaRepo) UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) { - _, err = mr.data.DB.ID(meta.ID).Update(meta) + _, err = mr.data.DB.Context(ctx).ID(meta.ID).Update(meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } +// AddOrUpdateMetaByObjectIdAndKey if exist record with same objectID and key, update it. Or create a new one +func (mr *metaRepo) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error { + _, err := mr.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) { + session = session.Context(ctx) + + // 1. acquire meta entity with target object id and key + metaEntity := &entity.Meta{} + exist, err := session.Where(builder.Eq{"object_id": objectId}.And(builder.Eq{"`key`": key})).ForUpdate().Get(metaEntity) + if err != nil { + return nil, err + } + + meta, err := f(metaEntity, exist) + if err != nil { + return nil, err + } + + // return entity.Meta + if exist { + _, err = session.ID(metaEntity.ID).Update(meta) + } else { + _, err = session.Insert(meta) + } + + return nil, err + }) + return err +} + // GetMetaByObjectIdAndKey get meta one func (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) ( meta *entity.Meta, exist bool, err error) { meta = &entity.Meta{} - exist, err = mr.data.DB.Where(builder.Eq{"object_id": objectID}.And(builder.Eq{"`key`": key})).Desc("created_at").Get(meta) + exist, err = mr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}.And(builder.Eq{"`key`": key})).Desc("created_at").Get(meta) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -65,17 +113,7 @@ func (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key s // GetMetaList get meta list all func (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaList []*entity.Meta, err error) { metaList = make([]*entity.Meta, 0) - err = mr.data.DB.Find(metaList, meta) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetMetaPage get meta page -func (mr *metaRepo) GetMetaPage(ctx context.Context, page, pageSize int, meta *entity.Meta) (metaList []*entity.Meta, total int64, err error) { - metaList = make([]*entity.Meta, 0) - total, err = pager.Help(page, pageSize, metaList, meta, mr.data.DB.NewSession()) + err = mr.data.DB.Context(ctx).Find(&metaList, meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index deaf431aa..8d8e707f2 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -1,15 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package notification import ( "context" "time" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - notficationcommon "github.com/answerdev/answer/internal/service/notification_common" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + notficationcommon "github.com/apache/answer/internal/service/notification_common" + "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" ) @@ -27,10 +47,10 @@ func NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo { // AddNotification add notification func (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) { - _, err = nr.data.DB.Insert(notification) + notification.ObjectID = uid.DeShortID(notification.ObjectID) + _, err = nr.data.DB.Context(ctx).Insert(notification) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -38,10 +58,10 @@ func (nr *notificationRepo) AddNotification(ctx context.Context, notification *e func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) { now := time.Now() notification.UpdatedAt = now - _, err = nr.data.DB.Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification) + notification.ObjectID = uid.DeShortID(notification.ObjectID) + _, err = nr.data.DB.Context(ctx).Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -49,10 +69,9 @@ func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notif func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Where("user_id =?", userID).And("type =?", notificationType).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("type = ?", notificationType).Cols("is_read").Update(info) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -60,17 +79,16 @@ func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, noti func (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Where("user_id =?", userID).And("id =?", id).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("id = ?", id).Cols("is_read").Update(info) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) { info := &entity.Notification{} - exist, err := nr.data.DB.Where("id = ? ", id).Get(info) + exist, err := nr.data.DB.Context(ctx).Where("id = ? ", id).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err @@ -80,7 +98,7 @@ func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Not func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) { info := &entity.Notification{} - exist, err := nr.data.DB.Where("user_id = ? ", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) + exist, err := nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err @@ -88,32 +106,50 @@ func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userI return info, exist, nil } -func (nr *notificationRepo) SearchList(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) { - var count int64 - var err error +func (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( + notificationList []*entity.Notification, total int64, err error) { + notificationList = make([]*entity.Notification, 0) + if searchCond.UserID == "" { + return notificationList, 0, nil + } - rows := make([]*entity.Notification, 0) - if search.UserID == "" { - return rows, 0, nil + session := nr.data.DB.Context(ctx) + session = session.Desc("updated_at") + + cond := &entity.Notification{ + UserID: searchCond.UserID, + Type: searchCond.Type, } - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 + if searchCond.InboxType > 0 { + cond.MsgType = searchCond.InboxType } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + total, err = pager.Help(searchCond.Page, searchCond.PageSize, ¬ificationList, cond, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - offset := search.Page * search.PageSize - session := nr.data.DB.Where("") - session = session.And("user_id = ?", search.UserID) - session = session.And("type = ?", search.Type) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) + return +} + +func (nr *notificationRepo) CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) { + count, err := nr.data.DB.Context(ctx).Count(cond) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return rows, count, err } - return rows, count, nil + return count, err +} + +func (nr *notificationRepo) DeleteNotification(ctx context.Context, userID string) (err error) { + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.Notification{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (nr *notificationRepo) DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) { + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.UserNotificationConfig{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return } diff --git a/internal/repo/plugin_config/plugin_config_repo.go b/internal/repo/plugin_config/plugin_config_repo.go new file mode 100644 index 000000000..8208fb75a --- /dev/null +++ b/internal/repo/plugin_config/plugin_config_repo.go @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin_config + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/plugin_common" + "github.com/segmentfault/pacman/errors" +) + +type pluginConfigRepo struct { + data *data.Data +} + +// NewPluginConfigRepo new repository +func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo { + return &pluginConfigRepo{ + data: data, + } +} + +func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) { + old := &entity.PluginConfig{PluginSlugName: pluginSlugName} + exist, err := ur.data.DB.Context(ctx).Get(old) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + old.Value = configValue + _, err = ur.data.DB.Context(ctx).ID(old.ID).Update(old) + } else { + _, err = ur.data.DB.Context(ctx).Insert(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue}) + } + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) { + pluginConfigs = make([]*entity.PluginConfig, 0) + err = ur.data.DB.Context(ctx).Find(&pluginConfigs) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return pluginConfigs, err +} diff --git a/internal/repo/plugin_config/plugin_user_config_repo.go b/internal/repo/plugin_config/plugin_user_config_repo.go new file mode 100644 index 000000000..19d6af5f9 --- /dev/null +++ b/internal/repo/plugin_config/plugin_user_config_repo.go @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin_config + +import ( + "context" + "github.com/apache/answer/internal/base/pager" + "xorm.io/xorm" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/plugin_common" + "github.com/segmentfault/pacman/errors" +) + +type pluginUserConfigRepo struct { + data *data.Data +} + +// NewPluginUserConfigRepo new repository +func NewPluginUserConfigRepo(data *data.Data) plugin_common.PluginUserConfigRepo { + return &pluginUserConfigRepo{ + data: data, + } +} + +func (ur *pluginUserConfigRepo) SaveUserPluginConfig(ctx context.Context, userID string, + pluginSlugName, configValue string) (err error) { + _, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) { + session = session.Context(ctx) + old := &entity.PluginUserConfig{ + UserID: userID, + PluginSlugName: pluginSlugName, + } + exist, err := session.Get(old) + if err != nil { + return nil, err + } + if exist { + old.Value = configValue + _, err = session.ID(old.ID).Update(old) + } else { + _, err = session.Insert(&entity.PluginUserConfig{ + UserID: userID, + PluginSlugName: pluginSlugName, + Value: configValue, + }) + } + if err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (ur *pluginUserConfigRepo) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) ( + pluginUserConfig *entity.PluginUserConfig, exist bool, err error) { + pluginUserConfig = &entity.PluginUserConfig{ + UserID: userID, + PluginSlugName: pluginSlugName, + } + exist, err = ur.data.DB.Context(ctx).Get(pluginUserConfig) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return pluginUserConfig, exist, err +} + +func (ur *pluginUserConfigRepo) GetPluginUserConfigPage(ctx context.Context, page, pageSize int) ( + pluginUserConfigs []*entity.PluginUserConfig, total int64, err error) { + pluginUserConfigs = make([]*entity.PluginUserConfig, 0) + total, err = pager.Help(page, pageSize, &pluginUserConfigs, &entity.PluginUserConfig{}, ur.data.DB.Context(ctx)) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (ur *pluginUserConfigRepo) DeleteUserPluginConfig(ctx context.Context, userID string) (err error) { + _, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.PluginUserConfig{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 16c751ec0..02f27f62f 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -1,31 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package repo import ( - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/repo/activity" - "github.com/answerdev/answer/internal/repo/activity_common" - "github.com/answerdev/answer/internal/repo/auth" - "github.com/answerdev/answer/internal/repo/captcha" - "github.com/answerdev/answer/internal/repo/collection" - "github.com/answerdev/answer/internal/repo/comment" - "github.com/answerdev/answer/internal/repo/common" - "github.com/answerdev/answer/internal/repo/config" - "github.com/answerdev/answer/internal/repo/export" - "github.com/answerdev/answer/internal/repo/meta" - "github.com/answerdev/answer/internal/repo/notification" - "github.com/answerdev/answer/internal/repo/rank" - "github.com/answerdev/answer/internal/repo/reason" - "github.com/answerdev/answer/internal/repo/report" - "github.com/answerdev/answer/internal/repo/revision" - "github.com/answerdev/answer/internal/repo/tag" - "github.com/answerdev/answer/internal/repo/unique" - "github.com/answerdev/answer/internal/repo/user" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/repo/activity" + "github.com/apache/answer/internal/repo/activity_common" + "github.com/apache/answer/internal/repo/answer" + "github.com/apache/answer/internal/repo/auth" + "github.com/apache/answer/internal/repo/badge" + "github.com/apache/answer/internal/repo/badge_award" + "github.com/apache/answer/internal/repo/badge_group" + "github.com/apache/answer/internal/repo/captcha" + "github.com/apache/answer/internal/repo/collection" + "github.com/apache/answer/internal/repo/comment" + "github.com/apache/answer/internal/repo/config" + "github.com/apache/answer/internal/repo/export" + "github.com/apache/answer/internal/repo/file_record" + "github.com/apache/answer/internal/repo/limit" + "github.com/apache/answer/internal/repo/meta" + "github.com/apache/answer/internal/repo/notification" + "github.com/apache/answer/internal/repo/plugin_config" + "github.com/apache/answer/internal/repo/question" + "github.com/apache/answer/internal/repo/rank" + "github.com/apache/answer/internal/repo/reason" + "github.com/apache/answer/internal/repo/report" + "github.com/apache/answer/internal/repo/review" + "github.com/apache/answer/internal/repo/revision" + "github.com/apache/answer/internal/repo/role" + "github.com/apache/answer/internal/repo/search_common" + "github.com/apache/answer/internal/repo/site_info" + "github.com/apache/answer/internal/repo/tag" + "github.com/apache/answer/internal/repo/tag_common" + "github.com/apache/answer/internal/repo/unique" + "github.com/apache/answer/internal/repo/user" + "github.com/apache/answer/internal/repo/user_external_login" + "github.com/apache/answer/internal/repo/user_notification_config" "github.com/google/wire" ) // ProviderSetRepo is data providers. var ProviderSetRepo = wire.NewSet( - common.NewCommonRepo, data.NewData, data.NewDB, data.NewCache, @@ -38,26 +70,43 @@ var ProviderSetRepo = wire.NewSet( activity_common.NewVoteRepo, config.NewConfigRepo, user.NewUserRepo, - user.NewUserBackyardRepo, + user.NewUserAdminRepo, rank.NewUserRankRepo, - NewQuestionRepo, - NewAnswerRepo, - NewActivityRepo, + question.NewQuestionRepo, + answer.NewAnswerRepo, + activity_common.NewActivityRepo, activity.NewVoteRepo, activity.NewFollowRepo, activity.NewAnswerActivityRepo, - activity.NewQuestionActivityRepo, activity.NewUserActiveActivityRepo, + activity.NewActivityRepo, + activity.NewReviewActivityRepo, tag.NewTagRepo, - tag.NewTagListRepo, + tag_common.NewTagCommonRepo, + tag.NewTagRelRepo, collection.NewCollectionRepo, collection.NewCollectionGroupRepo, auth.NewAuthRepo, revision.NewRevisionRepo, - NewSearchRepo, + search_common.NewSearchRepo, meta.NewMetaRepo, export.NewEmailRepo, reason.NewReasonRepo, - NewSiteInfo, + site_info.NewSiteInfo, notification.NewNotificationRepo, + role.NewRoleRepo, + role.NewUserRoleRelRepo, + role.NewRolePowerRelRepo, + role.NewPowerRepo, + user_external_login.NewUserExternalLoginRepo, + plugin_config.NewPluginConfigRepo, + user_notification_config.NewUserNotificationConfigRepo, + limit.NewRateLimitRepo, + plugin_config.NewPluginUserConfigRepo, + review.NewReviewRepo, + badge.NewBadgeRepo, + badge.NewEventRuleRepo, + badge_group.NewBadgeGroupRepo, + badge_award.NewBadgeAwardRepo, + file_record.NewFileRecordRepo, ) diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go new file mode 100644 index 000000000..3935e6102 --- /dev/null +++ b/internal/repo/question/question_repo.go @@ -0,0 +1,873 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package question + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + "unicode" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" + "xorm.io/xorm" +) + +// questionRepo question repository +type questionRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +// NewQuestionRepo new repository +func NewQuestionRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, +) questioncommon.QuestionRepo { + return &questionRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +// AddQuestion add question +func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Question) (err error) { + question.ID, err = qr.uniqueIDRepo.GenUniqueIDStr(ctx, question.TableName()) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _, err = qr.data.DB.Context(ctx).Insert(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } + return +} + +// RemoveQuestion delete question +func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) { + id = uid.DeShortID(id) + _, err = qr.data.DB.Context(ctx).Where("id =?", id).Delete(&entity.Question{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateQuestion update question +func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) { + question.ID = uid.DeShortID(question.ID) + _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols(Cols...).Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } + _ = qr.UpdateSearch(ctx, question.ID) + return +} + +func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) { + questionID = uid.DeShortID(questionID) + question := &entity.Question{} + _, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Incr("view_count", 1).Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, question.ID) + return nil +} + +func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) { + questionID = uid.DeShortID(questionID) + question := &entity.Question{} + question.AnswerCount = num + _, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Cols("answer_count").Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, question.ID) + return nil +} + +func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) { + questionID = uid.DeShortID(questionID) + _, err = qr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + count, err = session.Count(&entity.Collection{ObjectID: questionID}) + if err != nil { + return nil, err + } + + question := &entity.Question{CollectionCount: int(count)} + _, err = session.ID(questionID).MustCols("collection_count").Update(question) + if err != nil { + return nil, err + } + return + }) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) { + questionID = uid.DeShortID(questionID) + _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: status}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, questionID) + return nil +} + +func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) { + question.ID = uid.DeShortID(question.ID) + _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("status").Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, question.ID) + return nil +} + +func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) { + // get all deleted question ids + ids := make([]string, 0) + err = qr.data.DB.Context(ctx).Select("id").Table(new(entity.Question).TableName()). + Where("status = ?", entity.QuestionStatusDeleted).Find(&ids) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if len(ids) == 0 { + return nil + } + + // delete all revisions permanently + _, err = qr.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + _, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) { + questionID = uid.DeShortID(questionID) + _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: entity.QuestionStatusAvailable}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, questionID) + return nil +} + +func (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) { + question.ID = uid.DeShortID(question.ID) + _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("pin", "show").Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) { + question.ID = uid.DeShortID(question.ID) + _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("accepted_answer_id").Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, question.ID) + return nil +} + +func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) { + question.ID = uid.DeShortID(question.ID) + _, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("last_answer_id").Update(question) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + _ = qr.UpdateSearch(ctx, question.ID) + return nil +} + +// GetQuestion get question one +func (qr *questionRepo) GetQuestion(ctx context.Context, id string) ( + question *entity.Question, exist bool, err error, +) { + id = uid.DeShortID(id) + question = &entity.Question{} + question.ID = id + exist, err = qr.data.DB.Context(ctx).Where("id = ?", id).Get(question) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + question.ID = uid.EnShortID(question.ID) + } + return +} + +// GetQuestionsByTitle get question list by title +func (qr *questionRepo) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) ( + questionList []*entity.Question, err error) { + questionList = make([]*entity.Question, 0) + session := qr.data.DB.Context(ctx) + session.Where("status != ?", entity.QuestionStatusDeleted) + session.Where("title like ?", "%"+title+"%") + session.Limit(pageSize) + err = session.Find(&questionList) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + return +} + +func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) { + for key, itemID := range id { + id[key] = uid.DeShortID(itemID) + } + questionList = make([]*entity.Question, 0) + err = qr.data.DB.Context(ctx).Table("question").In("id", id).Find(&questionList) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + return +} + +// GetQuestionList get question list all +func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) { + question.ID = uid.DeShortID(question.ID) + questionList = make([]*entity.Question, 0) + err = qr.data.DB.Context(ctx).Find(&questionList, question) + if err != nil { + return questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, item := range questionList { + item.ID = uid.DeShortID(item.ID) + } + return +} + +func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { + session := qr.data.DB.Context(ctx) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) + count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (qr *questionRepo) GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) { + session := qr.data.DB.Context(ctx) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}). + And(builder.Eq{"answer_count": 0}) + count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (qr *questionRepo) GetResolvedQuestionCount(ctx context.Context) (count int64, err error) { + session := qr.data.DB.Context(ctx) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}). + And(builder.Neq{"answer_count": 0}). + And(builder.Neq{"accepted_answer_id": 0}) + count, err = session.Count(&entity.Question{Show: entity.QuestionShow}) + if err != nil { + return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) { + session := qr.data.DB.Context(ctx) + session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}) + count, err = session.Count(&entity.Question{UserID: userID, Show: show}) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) ( + questionIDList []*schema.SiteMapQuestionInfo, err error) { + page = page - 1 + questionIDList = make([]*schema.SiteMapQuestionInfo, 0) + + // try to get sitemap data from cache + cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page) + cacheData, exist, err := qr.data.Cache.GetString(ctx, cacheKey) + if err == nil && exist { + _ = json.Unmarshal([]byte(cacheData), &questionIDList) + return questionIDList, nil + } + + // get sitemap data from db + rows := make([]*entity.Question, 0) + session := qr.data.DB.Context(ctx) + session.Select("id,title,created_at,post_update_time") + session.Where("`show` = ?", entity.QuestionShow) + session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed) + session.Limit(pageSize, page*pageSize) + session.Asc("created_at") + err = session.Find(&rows) + if err != nil { + return questionIDList, err + } + + // warp data + for _, question := range rows { + item := &schema.SiteMapQuestionInfo{ID: question.ID} + if handler.GetEnableShortID(ctx) { + item.ID = uid.EnShortID(question.ID) + } + item.Title = htmltext.UrlTitle(question.Title) + if question.PostUpdateTime.IsZero() { + item.UpdateTime = question.CreatedAt.Format(time.RFC3339) + } else { + item.UpdateTime = question.PostUpdateTime.Format(time.RFC3339) + } + questionIDList = append(questionIDList, item) + } + + // set sitemap data to cache + cacheDataByte, _ := json.Marshal(questionIDList) + if err := qr.data.Cache.SetString(ctx, cacheKey, string(cacheDataByte), constant.SiteMapQuestionCacheTime); err != nil { + log.Error(err) + } + return questionIDList, nil +} + +// GetQuestionPage query question page +func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, + tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( + questionList []*entity.Question, total int64, err error) { + questionList = make([]*entity.Question, 0) + session := qr.data.DB.Context(ctx) + status := []int{entity.QuestionStatusAvailable} + if orderCond != "unanswered" { + status = append(status, entity.QuestionStatusClosed) + } + if showPending { + status = append(status, entity.QuestionStatusPending) + } + session.Select("question.*") + session.In("question.status", status) + if len(tagIDs) > 0 { + session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id") + session.In("tag_rel.tag_id", tagIDs) + session.And("tag_rel.status = ?", entity.TagRelStatusAvailable) + } + if len(userID) > 0 { + session.And("question.user_id = ?", userID) + if !showHidden { + session.And("question.show = ?", entity.QuestionShow) + } + } else { + session.And("question.show = ?", entity.QuestionShow) + } + if inDays > 0 { + session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays)) + } + + switch orderCond { + case "newest": + session.OrderBy("question.pin desc,question.created_at DESC") + case "active": + if inDays == 0 { + session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180)) + } + session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90)) + session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC") + case "hot": + session.OrderBy("question.pin desc,question.hot_score DESC") + case "score": + session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC") + case "unanswered": + session.Where("question.answer_count = 0") + session.OrderBy("question.pin desc,question.created_at DESC") + case "frequent": + session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC") + } + + session.GroupBy("question.id") + total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + return questionList, total, err +} + +// GetRecommendQuestionPageByTags get recommend question page by tags +func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) ( + questionList []*entity.Question, total int64, err error) { + questionList = make([]*entity.Question, 0) + orderBySQL := "question.pin DESC, question.created_at DESC" + + // Please Make sure every question has at least one tag + selectSQL := entity.Question{}.TableName() + ".*" + if len(followedQuestionIDs) > 0 { + idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" + selectSQL += fmt.Sprintf(", CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END AS order_priority", idStr) + orderBySQL = "order_priority, " + orderBySQL + } + session := qr.data.DB.Context(ctx).Select(selectSQL) + + if len(tagIDs) > 0 { + session.Where("question.user_id != ?", userID). + And("question.id NOT IN (SELECT question_id FROM answer WHERE user_id = ?)", userID). + Join("INNER", "tag_rel", "question.id = tag_rel.object_id"). + And("tag_rel.status = ?", entity.TagRelStatusAvailable). + Join("INNER", "tag", "tag.id = tag_rel.tag_id"). + In("tag.id", tagIDs) + } else if len(followedQuestionIDs) == 0 { + return questionList, 0, nil + } + + if len(followedQuestionIDs) > 0 { + if len(tagIDs) > 0 { + // if tags provided, show followed questions and tag questions + session.Or(builder.In("question.id", followedQuestionIDs)) + } else { + // if no tags, only show followed questions + session.Where(builder.In("question.id", followedQuestionIDs)) + } + } + + session. + And("question.show = ? and question.status = ?", entity.QuestionShow, entity.QuestionStatusAvailable). + Distinct("question.id"). + OrderBy(orderBySQL) + + total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + + return questionList, total, err +} + +func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) { + var ( + count int64 + err error + session = qr.data.DB.Context(ctx).Table("question") + ) + + session.Where(builder.Eq{ + "status": search.Status, + }) + + rows := make([]*entity.Question, 0) + if search.Page > 0 { + search.Page = search.Page - 1 + } else { + search.Page = 0 + } + if search.PageSize == 0 { + search.PageSize = constant.DefaultPageSize + } + + // search by question title like or question id + if len(search.Query) > 0 { + // check id search + var ( + idSearch = false + id = "" + ) + + if strings.Contains(search.Query, "question:") { + idSearch = true + id = strings.TrimSpace(strings.TrimPrefix(search.Query, "question:")) + id = uid.DeShortID(id) + for _, r := range id { + if !unicode.IsDigit(r) { + idSearch = false + break + } + } + } + + if idSearch { + session.And(builder.Eq{ + "id": id, + }) + } else { + session.And(builder.Like{ + "title", search.Query, + }) + } + } + + offset := search.Page * search.PageSize + + session.OrderBy("created_at desc"). + Limit(search.PageSize, offset) + count, err = session.FindAndCount(&rows) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return rows, count, err + } + if handler.GetEnableShortID(ctx) { + for _, item := range rows { + item.ID = uid.EnShortID(item.ID) + } + } + return rows, count, nil +} + +// UpdateSearch update search, if search plugin not enable, do nothing +func (qr *questionRepo) UpdateSearch(ctx context.Context, questionID string) (err error) { + // check search plugin + var s plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + s = search + return nil + }) + if s == nil { + return + } + questionID = uid.DeShortID(questionID) + question, exist, err := qr.GetQuestion(ctx, questionID) + if !exist { + return + } + if err != nil { + return err + } + + // get tags + var ( + tagListList = make([]*entity.TagRel, 0) + tags = make([]string, 0) + ) + session := qr.data.DB.Context(ctx).Where("object_id = ?", questionID) + session.Where("status = ?", entity.TagRelStatusAvailable) + err = session.Find(&tagListList) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + content := &plugin.SearchContent{ + ObjectID: questionID, + Title: question.Title, + Type: constant.QuestionObjectType, + Content: question.OriginalText, + Answers: int64(question.AnswerCount), + Status: plugin.SearchContentStatus(question.Status), + Tags: tags, + QuestionID: questionID, + UserID: question.UserID, + Views: int64(question.ViewCount), + Created: question.CreatedAt.Unix(), + Active: question.UpdatedAt.Unix(), + Score: int64(question.VoteCount), + HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", + } + err = s.UpdateContent(ctx, content) + return +} + +func (qr *questionRepo) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) { + // get all question id that need to be deleted + questionIDs := make([]string, 0) + session := qr.data.DB.Context(ctx).Where("user_id = ?", userID) + session.Where("status != ?", entity.QuestionStatusDeleted) + err = session.Select("id").Table("question").Find(&questionIDs) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if len(questionIDs) == 0 { + return nil + } + + log.Infof("find %d questions need to be deleted for user %s", len(questionIDs), userID) + + // delete all question + session = qr.data.DB.Context(ctx).Where("user_id = ?", userID) + session.Where("status != ?", entity.QuestionStatusDeleted) + _, err = session.Cols("status", "updated_at").Update(&entity.Question{ + UpdatedAt: time.Now(), + Status: entity.QuestionStatusDeleted, + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // update search content + for _, id := range questionIDs { + _ = qr.UpdateSearch(ctx, id) + } + return nil +} + +// LinkQuestion batch insert question link +func (qr *questionRepo) LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) { + // Batch retrieve all links + var links []*entity.QuestionLink + for _, l := range link { + l.FromQuestionID = uid.DeShortID(l.FromQuestionID) + l.ToQuestionID = uid.DeShortID(l.ToQuestionID) + l.FromAnswerID = uid.DeShortID(l.FromAnswerID) + l.ToAnswerID = uid.DeShortID(l.ToAnswerID) + links = append(links, l) + } + // Retrieve existing records from the database + var existLinks []*entity.QuestionLink + session := qr.data.DB.Context(ctx) + for _, link := range links { + session = session.Or(builder.Eq{ + "from_question_id": link.FromQuestionID, + "to_question_id": link.ToQuestionID, + "from_answer_id": link.FromAnswerID, + "to_answer_id": link.ToAnswerID, + }) + } + err = session.Find(&existLinks) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // Optimize separation of records that need to be updated or inserted using a map + existMap := make(map[string]*entity.QuestionLink) + for _, el := range existLinks { + key := fmt.Sprintf("%s:%s:%s:%s", el.FromQuestionID, el.ToQuestionID, el.FromAnswerID, el.ToAnswerID) + existMap[key] = el + } + + var updateLinks []*entity.QuestionLink + var insertLinks []*entity.QuestionLink + for _, link := range links { + key := fmt.Sprintf("%s:%s:%s:%s", link.FromQuestionID, link.ToQuestionID, link.FromAnswerID, link.ToAnswerID) + if el, exist := existMap[key]; exist { + if el.Status == entity.QuestionLinkStatusDeleted { + el.Status = entity.QuestionLinkStatusAvailable + el.UpdatedAt = time.Now() + updateLinks = append(updateLinks, el) + } + } else { + link.Status = entity.QuestionLinkStatusAvailable + link.CreatedAt = time.Now() + link.UpdatedAt = time.Now() + insertLinks = append(insertLinks, link) + } + } + + // Batch update + if len(updateLinks) > 0 { + for _, link := range updateLinks { + _, err = qr.data.DB.Context(ctx).ID(link.ID).Cols("status").Update(&entity.QuestionLink{Status: entity.QuestionLinkStatusAvailable}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + } + } + + // Batch insert + if len(insertLinks) > 0 { + _, err = qr.data.DB.Context(ctx).Insert(insertLinks) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + } + + return +} + +// UpdateQuestionLinkCount update question link count +func (qr *questionRepo) UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) { + // count the number of links + count, err := qr.data.DB.Context(ctx). + Where("to_question_id = ?", questionID). + Where("status = ?", entity.QuestionLinkStatusAvailable). + Count(&entity.QuestionLink{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // update the number of links + _, err = qr.data.DB.Context(ctx).ID(questionID). + Cols("linked_count").Update(&entity.Question{LinkedCount: int(count)}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetLinkedQuestionIDs get linked question ids +func (qr *questionRepo) GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) ( + questionIDs []string, err error) { + questionIDs = make([]string, 0) + err = qr.data.DB.Context(ctx). + Select("to_question_id"). + Table(new(entity.QuestionLink).TableName()). + Where("from_question_id = ?", questionID). + Where("status = ?", status). + Find(&questionIDs) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return questionIDs, nil +} + +// RecoverQuestionLink batch recover question link +func (qr *questionRepo) RecoverQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) { + return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusAvailable, links...) +} + +// RemoveQuestionLink batch remove question link +func (qr *questionRepo) RemoveQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) { + return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusDeleted, links...) +} + +// UpdateQuestionLinkStatus update question link status +func (qr *questionRepo) UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) { + if len(links) == 0 { + return nil + } + + session := qr.data.DB.Context(ctx).Cols("status") + for _, link := range links { + eq := builder.Eq{} + if link.FromQuestionID != "" { + eq["from_question_id"] = uid.DeShortID(link.FromQuestionID) + } + if link.FromAnswerID != "" { + eq["from_answer_id"] = uid.DeShortID(link.FromAnswerID) + } + if link.ToQuestionID != "" { + eq["to_question_id"] = uid.DeShortID(link.ToQuestionID) + } + if link.ToAnswerID != "" { + eq["to_answer_id"] = uid.DeShortID(link.ToAnswerID) + } + session = session.Or(eq) + } + _, err = session.Update(&entity.QuestionLink{Status: status}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetQuestionLink get linked question to questionID +func (qr *questionRepo) GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questionList []*entity.Question, total int64, err error) { + questionList = make([]*entity.Question, 0) + questionID = uid.DeShortID(questionID) + questionStatus := []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed, entity.QuestionStatusPending} + if questionID == "0" { + return nil, 0, errors.InternalServer(reason.DatabaseError).WithError( + fmt.Errorf("questionID is empty"), + ).WithStack() + } + + session := qr.data.DB.Context(ctx). + Table("question_link"). + Join("INNER", "question", "question_link.from_question_id = question.id"). + Where("question_link.to_question_id = ? AND question.show = ?", questionID, entity.QuestionShow). + Distinct("question.id"). + Where("question_link.status = ?", entity.QuestionLinkStatusAvailable). + Select("question.*"). + In("question.status", questionStatus) + + switch orderCond { + case "newest": + session.OrderBy("question.pin desc,question.created_at DESC") + case "active": + if inDays == 0 { + session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180)) + } + session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90)) + session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC") + case "hot": + session.OrderBy("question.pin desc,question.hot_score DESC") + case "score": + session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC") + case "unanswered": + session.Where("question.answer_count = 0") + session.OrderBy("question.pin desc,question.created_at DESC") + case "frequent": + session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC") + } + + if page > 0 && pageSize > 0 { + session.Limit(pageSize, (page-1)*pageSize) + } + + total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + return +} diff --git a/internal/repo/question_repo.go b/internal/repo/question_repo.go deleted file mode 100644 index f6f4e40b3..000000000 --- a/internal/repo/question_repo.go +++ /dev/null @@ -1,250 +0,0 @@ -package repo - -import ( - "context" - "time" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/unique" - - "github.com/segmentfault/pacman/errors" -) - -// questionRepo question repository -type questionRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo -} - -// NewQuestionRepo new repository -func NewQuestionRepo( - data *data.Data, - uniqueIDRepo unique.UniqueIDRepo, -) questioncommon.QuestionRepo { - return &questionRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - } -} - -// AddQuestion add question -func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Question) (err error) { - question.ID, err = qr.uniqueIDRepo.GenUniqueIDStr(ctx, question.TableName()) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - _, err = qr.data.DB.Insert(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// RemoveQuestion delete question -func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) { - _, err = qr.data.DB.Where("id =?", id).Delete(&entity.Question{}) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// UpdateQuestion update question -func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) { - _, err = qr.data.DB.Where("id =?", question.ID).Cols(Cols...).Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionId string) (err error) { - question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("view_count", 1).Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionId string, num int) (err error) { - question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("answer_count", num).Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionId string, num int) (err error) { - question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("collection_count", num).Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error) { - now := time.Now() - question.UpdatedAt = now - _, err = qr.data.DB.Where("id =?", question.ID).Cols("status", "updated_at").Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) { - _, err = qr.data.DB.Where("id =?", question.ID).Cols("accepted_answer_id").Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) { - _, err = qr.data.DB.Where("id =?", question.ID).Cols("last_answer_id").Update(question) - if err != nil { - return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return nil -} - -// GetQuestion get question one -func (qr *questionRepo) GetQuestion(ctx context.Context, id string) ( - question *entity.Question, exist bool, err error) { - question = &entity.Question{} - question.ID = id - exist, err = qr.data.DB.Where("id = ?", id).Get(question) - if err != nil { - return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetTagBySlugName get tag by slug name -func (qr *questionRepo) SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error) { - questionList = make([]*entity.Question, 0) - err = qr.data.DB.Table("question").Where("title like ?", "%"+title+"%").Limit(10, 0).Find(&questionList) - if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) { - questionList = make([]*entity.Question, 0) - err = qr.data.DB.Table("question").In("id", id).Find(&questionList) - if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetQuestionList get question list all -func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) { - questionList = make([]*entity.Question, 0) - err = qr.data.DB.Find(questionList, question) - if err != nil { - return questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetQuestionPage get question page -func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) { - questionList = make([]*entity.Question, 0) - total, err = pager.Help(page, pageSize, questionList, question, qr.data.DB.NewSession()) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// SearchList -func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error) { - var count int64 - var err error - rows := make([]*entity.QuestionTag, 0) - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 - } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize - } - offset := search.Page * search.PageSize - session := qr.data.DB.Table("question") - - if len(search.TagIDs) > 0 { - session = session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id") - session = session.And("tag_rel.tag_id =?", search.TagIDs[0]) - //session = session.In("tag_rel.tag_id ", search.TagIDs) - session = session.And("tag_rel.status =?", entity.TagRelStatusAvailable) - } - - if len(search.UserID) > 0 { - session = session.And("question.user_id = ?", search.UserID) - } - - session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}) - // if search.Status > 0 { - // session = session.And("question.status = ?", search.Status) - // } - //switch - //newest, active,frequent,score,unanswered - switch search.Order { - case "newest": - session = session.OrderBy("question.created_at desc") - case "active": - session = session.OrderBy("question.post_update_time desc,question.updated_at desc") - case "frequent": - session = session.OrderBy("question.view_count desc") - case "score": - session = session.OrderBy("question.vote_count desc,question.view_count desc") - case "unanswered": - session = session.And("question.last_answer_id = 0") - session = session.OrderBy("question.created_at desc") - } - session = session.Limit(search.PageSize, offset) - session = session.Select("question.id,question.user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id") - count, err = session.FindAndCount(&rows) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return rows, count, err - } - return rows, count, nil -} - -func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) { - var count int64 - var err error - rows := make([]*entity.Question, 0) - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 - } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize - } - offset := search.Page * search.PageSize - session := qr.data.DB.Table("question") - session = session.And("status =?", search.Status) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return rows, count, err - } - return rows, count, nil -} diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go index 8f473e536..cb9ca1b3d 100644 --- a/internal/repo/rank/user_rank_repo.go +++ b/internal/repo/rank/user_rank_repo.go @@ -1,14 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package rank import ( "context" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/rank" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/plugin" "github.com/jinzhu/now" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -18,42 +38,95 @@ import ( // UserRankRepo user rank repository type UserRankRepo struct { - data *data.Data - configRepo config.ConfigRepo + data *data.Data + configService *config.ConfigService } // NewUserRankRepo new repository -func NewUserRankRepo(data *data.Data, configRepo config.ConfigRepo) rank.UserRankRepo { +func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.UserRankRepo { return &UserRankRepo{ - data: data, - configRepo: configRepo, + data: data, + configService: configService, + } +} + +func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) { + maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit") + if err != nil { + return 0, err + } + return maxDailyRank, nil +} + +func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session, + userID string, maxDailyRank int) ( + reach bool, err error) { + session.Where(builder.Eq{"user_id": userID}) + session.Where(builder.Eq{"cancelled": 0}) + session.Where(builder.Between{ + Col: "updated_at", + LessVal: now.BeginningOfDay(), + MoreVal: now.EndOfDay(), + }) + + earned, err := session.SumInt(&entity.Activity{}, "`rank`") + if err != nil { + return false, err + } + if int(earned) < maxDailyRank { + return false, nil } + log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank) + return true, nil +} + +// ChangeUserRank change user rank +func (ur *UserRankRepo) ChangeUserRank( + ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) { + // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. + if plugin.RankAgentEnabled() || deltaRank == 0 { + return nil + } + + // If user rank is lower than 1 after this action, then user rank will be set to 1 only. + if deltaRank < 0 && userCurrentScore+deltaRank < 1 { + deltaRank = 1 - userCurrentScore + } + + _, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{}) + if err != nil { + return err + } + return nil } // TriggerUserRank trigger user rank change // session is need provider, it means this action must be success or failure // if outer action is failed then this action is need rollback func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, - session *xorm.Session, userId string, deltaRank int, activityType int) (isReachStandard bool, err error) { - if deltaRank == 0 { + session *xorm.Session, userID string, deltaRank int, activityType int, +) (isReachStandard bool, err error) { + // IMPORTANT: If user center enabled the rank agent, then we should not change user rank. + if plugin.RankAgentEnabled() || deltaRank == 0 { return false, nil } if deltaRank < 0 { // if user rank is lower than 1 after this action, then user rank will be set to 1 only. - isReachMin, err := ur.checkUserMinRank(ctx, session, userId, activityType) + var isReachMin bool + isReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if isReachMin { - _, err = session.Where(builder.Eq{"id": userId}).Update(&entity.User{Rank: 1}) + _, err = session.Where(builder.Eq{"id": userID}).Update(&entity.User{Rank: 1}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return false, nil + return true, nil } } else { - isReachStandard, err = ur.checkUserTodayRank(ctx, session, userId, activityType) + isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -61,7 +134,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, return isReachStandard, nil } } - _, err = session.Where(builder.Eq{"id": userId}).Incr("`rank`", deltaRank).Update(&entity.User{}) + _, err = session.Where(builder.Eq{"id": userID}).Incr("`rank`", deltaRank).Update(&entity.User{}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -69,9 +142,10 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, } func (ur *UserRankRepo) checkUserMinRank(ctx context.Context, session *xorm.Session, userID string, deltaRank int) ( - isReachStandard bool, err error) { + isReachStandard bool, err error, +) { bean := &entity.User{ID: userID} - _, err = session.Select("rank").Get(bean) + _, err = session.Select("`rank`").Get(bean) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -83,15 +157,16 @@ func (ur *UserRankRepo) checkUserMinRank(ctx context.Context, session *xorm.Sess } func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, - session *xorm.Session, userID string, activityType int) (isReachStandard bool, err error) { + session *xorm.Session, userID string, activityType int, +) (isReachStandard bool, err error) { // exclude daily rank - exclude, err := ur.configRepo.GetArrayString("daily_rank_limit.exclude") + exclude, _ := ur.configService.GetArrayStringValue(ctx, "daily_rank_limit.exclude") for _, item := range exclude { - excludeActivityType, err := ur.configRepo.GetInt(item) + cfg, err := ur.configService.GetConfigByKey(ctx, item) if err != nil { return false, err } - if activityType == excludeActivityType { + if activityType == cfg.ID { return false, nil } } @@ -105,13 +180,13 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, LessVal: start, MoreVal: end, }) - earned, err := session.Sum(&entity.Activity{}, "rank") + earned, err := session.SumInt(&entity.Activity{}, "`rank`") if err != nil { return false, err } // max rank - maxDailyRank, err := ur.configRepo.GetInt("daily_rank_limit") + maxDailyRank, err := ur.configService.GetIntValue(ctx, "daily_rank_limit") if err != nil { return false, err } @@ -123,14 +198,15 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, return true, nil } -func (ur *UserRankRepo) UserRankPage(ctx context.Context, userId string, page, pageSize int) ( - rankPage []*entity.Activity, total int64, err error) { +func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, pageSize int) ( + rankPage []*entity.Activity, total int64, err error, +) { rankPage = make([]*entity.Activity, 0) - session := ur.data.DB.Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})) + session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"`rank`": 0}) session.Desc("created_at") - cond := &entity.Activity{UserID: userId} + cond := &entity.Activity{UserID: userID} total, err = pager.Help(page, pageSize, &rankPage, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/reason/reason_repo.go b/internal/repo/reason/reason_repo.go index 1576df6e9..875e3f983 100644 --- a/internal/repo/reason/reason_repo.go +++ b/internal/repo/reason/reason_repo.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package reason import ( @@ -5,59 +24,48 @@ import ( "encoding/json" "fmt" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/reason_common" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/reason_common" "github.com/segmentfault/pacman/log" ) type reasonRepo struct { - configRepo config.ConfigRepo + configService *config.ConfigService } -func NewReasonRepo(configRepo config.ConfigRepo) reason_common.ReasonRepo { +func NewReasonRepo(configService *config.ConfigService) reason_common.ReasonRepo { return &reasonRepo{ - configRepo: configRepo, + configService: configService, } } -func (rr *reasonRepo) ListReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) { - var ( - reasonAction = fmt.Sprintf("%s.%s.reasons", req.ObjectType, req.Action) - reasonKeys []string - cfgValue string - ) - resp = []schema.ReasonItem{} +func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) { + lang := handler.GetLangByCtx(ctx) + reasonAction := fmt.Sprintf("%s.%s.reasons", objectType, action) + resp = make([]*schema.ReasonItem, 0) - reasonKeys, err = rr.configRepo.GetArrayString(reasonAction) + reasonKeys, err := rr.configService.GetArrayStringValue(ctx, reasonAction) if err != nil { - return + return nil, err } for _, reasonKey := range reasonKeys { - var ( - reasonType int - reason = schema.ReasonItem{} - ) - - cfgValue, err = rr.configRepo.GetString(reasonKey) + cfg, err := rr.configService.GetConfigByKey(ctx, reasonKey) if err != nil { log.Error(err) continue } - err = json.Unmarshal([]byte(cfgValue), &reason) + reason := &schema.ReasonItem{} + err = json.Unmarshal(cfg.GetByteValue(), reason) if err != nil { log.Error(err) continue } - reasonType, err = rr.configRepo.GetConfigType(reasonKey) - if err != nil { - log.Error(err) - continue - } - - reason.ReasonType = reasonType + reason.Translate(reasonKey, lang) + reason.ReasonType = cfg.ID resp = append(resp, reason) } - return + return resp, nil } diff --git a/internal/repo/repo_test/auth_test.go b/internal/repo/repo_test/auth_test.go new file mode 100644 index 000000000..387332399 --- /dev/null +++ b/internal/repo/repo_test/auth_test.go @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/auth" + "github.com/stretchr/testify/assert" +) + +var ( + accessToken = "token" + visitToken = "visitToken" + userID = "1" +) + +func Test_authRepo_SetUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} + +func Test_authRepo_RemoveUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + + userInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + assert.Nil(t, userInfo) +} + +func Test_authRepo_SetUserStatus(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetUserStatus(context.TODO(), userID) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} +func Test_authRepo_RemoveUserStatus(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveUserStatus(context.TODO(), userID) + assert.NoError(t, err) + + userInfo, err := authRepo.GetUserStatus(context.TODO(), userID) + assert.NoError(t, err) + assert.Nil(t, userInfo) +} + +func Test_authRepo_SetAdminUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} + +func Test_authRepo_RemoveAdminUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveAdminUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + + userInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken) + assert.NoError(t, err) + assert.Nil(t, userInfo) +} diff --git a/internal/repo/repo_test/captcha_test.go b/internal/repo/repo_test/captcha_test.go new file mode 100644 index 000000000..48e9dd806 --- /dev/null +++ b/internal/repo/repo_test/captcha_test.go @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/repo/captcha" + "github.com/stretchr/testify/assert" +) + +var ( + ip = "127.0.0.1" + actionType = "actionType" + amount = 1 +) + +func Test_captchaRepo_DelActionType(t *testing.T) { + captchaRepo := captcha.NewCaptchaRepo(testDataSource) + err := captchaRepo.SetActionType(context.TODO(), ip, actionType, "", amount) + assert.NoError(t, err) + + actionInfo, err := captchaRepo.GetActionType(context.TODO(), ip, actionType) + assert.NoError(t, err) + assert.Equal(t, amount, actionInfo.Num) + + err = captchaRepo.DelActionType(context.TODO(), ip, actionType) + assert.NoError(t, err) +} + +func Test_captchaRepo_SetCaptcha(t *testing.T) { + captchaRepo := captcha.NewCaptchaRepo(testDataSource) + key, capt := "key", "1234" + err := captchaRepo.SetCaptcha(context.TODO(), key, capt) + assert.NoError(t, err) + + gotCaptcha, err := captchaRepo.GetCaptcha(context.TODO(), key) + assert.NoError(t, err) + assert.Equal(t, capt, gotCaptcha) +} diff --git a/internal/repo/repo_test/comment_repo_test.go b/internal/repo/repo_test/comment_repo_test.go new file mode 100644 index 000000000..3de154817 --- /dev/null +++ b/internal/repo/repo_test/comment_repo_test.go @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/comment" + "github.com/apache/answer/internal/repo/unique" + commentService "github.com/apache/answer/internal/service/comment" + "github.com/stretchr/testify/assert" +) + +func buildCommentEntity() *entity.Comment { + return &entity.Comment{ + UserID: "1", + ObjectID: "1", + QuestionID: "1", + VoteCount: 1, + Status: entity.CommentStatusAvailable, + OriginalText: "# title", + ParsedText: "

Title

", + } +} + +func Test_commentRepo_AddComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) +} + +func Test_commentRepo_GetCommentPage(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + resp, total, err := commentRepo.GetCommentPage(context.TODO(), &commentService.CommentQuery{ + PageCond: pager.PageCond{ + Page: 1, + PageSize: 10, + }, + }) + assert.NoError(t, err) + assert.Equal(t, total, int64(1)) + assert.Equal(t, resp[0].ID, testCommentEntity.ID) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) +} + +func Test_commentRepo_UpdateComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + commonCommentRepo := comment.NewCommentCommonRepo(testDataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + testCommentEntity.ParsedText = "test" + err = commentRepo.UpdateCommentContent(context.TODO(), testCommentEntity.ID, "test", "test") + assert.NoError(t, err) + + newComment, exist, err := commonCommentRepo.GetComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testCommentEntity.ParsedText, newComment.ParsedText) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) +} + +func Test_commentRepo_CannotGetDeletedComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + + _, exist, err := commentRepo.GetComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + assert.False(t, exist) +} diff --git a/internal/repo/repo_test/email_repo_test.go b/internal/repo/repo_test/email_repo_test.go new file mode 100644 index 000000000..82fc6c571 --- /dev/null +++ b/internal/repo/repo_test/email_repo_test.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + "time" + + "github.com/apache/answer/internal/repo/export" + "github.com/stretchr/testify/assert" +) + +func Test_emailRepo_VerifyCode(t *testing.T) { + emailRepo := export.NewEmailRepo(testDataSource) + code, content := "1111", "{\"source_type\":\"\",\"e_mail\":\"\",\"user_id\":\"1\",\"skip_validation_latest_code\":false}" + err := emailRepo.SetCode(context.TODO(), "1", code, content, time.Minute) + assert.NoError(t, err) + + verifyContent, err := emailRepo.VerifyCode(context.TODO(), code) + assert.NoError(t, err) + assert.Equal(t, content, verifyContent) +} diff --git a/internal/repo/repo_test/meta_repo_test.go b/internal/repo/repo_test/meta_repo_test.go new file mode 100644 index 000000000..e910dab3f --- /dev/null +++ b/internal/repo/repo_test/meta_repo_test.go @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/meta" + "github.com/stretchr/testify/assert" +) + +func buildMetaEntity() *entity.Meta { + return &entity.Meta{ + ObjectID: "1", + Key: "1", + Value: "1", + } +} + +func Test_metaRepo_GetMetaByObjectIdAndKey(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, metaEnt.ID, gotMeta.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_GetMetaList(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) + assert.NoError(t, err) + assert.Equal(t, len(gotMetaList), 1) + assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_GetMetaPage(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) + assert.NoError(t, err) + assert.Equal(t, len(gotMetaList), 1) + assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_UpdateMeta(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + metaEnt.Value = "testing" + err = metaRepo.UpdateMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, gotMeta.Value, metaEnt.Value) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} diff --git a/internal/repo/repo_test/notification_repo_test.go b/internal/repo/repo_test/notification_repo_test.go new file mode 100644 index 000000000..a05913ba1 --- /dev/null +++ b/internal/repo/repo_test/notification_repo_test.go @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/notification" + "github.com/apache/answer/internal/schema" + "github.com/stretchr/testify/assert" +) + +func buildNotificationEntity() *entity.Notification { + return &entity.Notification{ + UserID: "1", + ObjectID: "1", + Content: "1", + Type: schema.NotificationTypeInbox, + IsRead: schema.NotificationNotRead, + Status: schema.NotificationStatusNormal, + } +} + +func Test_notificationRepo_ClearIDUnRead(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + err = notificationRepo.ClearIDUnRead(context.TODO(), ent.UserID, ent.ID) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, schema.NotificationRead, got.IsRead) +} + +func Test_notificationRepo_ClearUnRead(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + err = notificationRepo.ClearUnRead(context.TODO(), ent.UserID, ent.Type) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, schema.NotificationRead, got.IsRead) +} + +func Test_notificationRepo_GetById(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.ID, ent.ID) +} + +func Test_notificationRepo_GetByUserIdObjectIdTypeId(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetByUserIdObjectIdTypeId(context.TODO(), ent.UserID, ent.ObjectID, ent.Type) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.ObjectID, ent.ObjectID) +} + +func Test_notificationRepo_GetNotificationPage(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID}) + assert.NoError(t, err) + assert.True(t, total > 0) + assert.Equal(t, notificationPage[0].UserID, ent.UserID) +} + +func Test_notificationRepo_UpdateNotificationContent(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + ent.Content = "test" + err = notificationRepo.UpdateNotificationContent(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.Content, ent.Content) +} diff --git a/internal/repo/repo_test/reason_repo_test.go b/internal/repo/repo_test/reason_repo_test.go new file mode 100644 index 000000000..636f8d278 --- /dev/null +++ b/internal/repo/repo_test/reason_repo_test.go @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/repo/config" + serviceconfig "github.com/apache/answer/internal/service/config" + + "github.com/apache/answer/internal/repo/reason" + "github.com/stretchr/testify/assert" +) + +func Test_reasonRepo_ListReasons(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + reasonRepo := reason.NewReasonRepo(serviceconfig.NewConfigService(configRepo)) + reasonItems, err := reasonRepo.ListReasons(context.TODO(), "question", "close") + assert.NoError(t, err) + assert.Equal(t, 4, len(reasonItems)) +} diff --git a/internal/repo/repo_test/recommend_test.go b/internal/repo/repo_test/recommend_test.go new file mode 100644 index 000000000..a21693886 --- /dev/null +++ b/internal/repo/repo_test/recommend_test.go @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/activity" + "github.com/apache/answer/internal/repo/activity_common" + "github.com/apache/answer/internal/repo/config" + "github.com/apache/answer/internal/repo/question" + "github.com/apache/answer/internal/repo/tag" + "github.com/apache/answer/internal/repo/tag_common" + "github.com/apache/answer/internal/repo/unique" + "github.com/apache/answer/internal/repo/user" + config2 "github.com/apache/answer/internal/service/config" + "github.com/stretchr/testify/assert" +) + +func Test_questionRepo_GetRecommend(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) + userRepo = user.NewUserRepo(testDataSource) + tagRelRepo = tag.NewTagRelRepo(testDataSource, uniqueIDRepo) + tagRepo = tag.NewTagRepo(testDataSource, uniqueIDRepo) + tagCommenRepo = tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) + configRepo = config.NewConfigRepo(testDataSource) + configService = config2.NewConfigService(configRepo) + activityCommonRepo = activity_common.NewActivityRepo(testDataSource, uniqueIDRepo, configService) + followRepo = activity.NewFollowRepo(testDataSource, uniqueIDRepo, activityCommonRepo) + ) + + // create question and user + user := &entity.User{ + Username: "ferrischi201", + Pass: "ferrischi201", + EMail: "ferrischi201@example.com", + MailStatus: entity.EmailStatusAvailable, + Status: entity.UserStatusAvailable, + DisplayName: "ferrischi201", + IsAdmin: false, + } + err := userRepo.AddUser(context.TODO(), user) + assert.NoError(t, err) + assert.NotEqual(t, "", user.ID) + + questions := make([]*entity.Question, 0) + // tag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 1", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 1", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 3", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Invalid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 4", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 3", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 5", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + + for _, question := range questions { + err = questionRepo.AddQuestion(context.TODO(), question) + assert.NoError(t, err) + assert.NotEqual(t, "", question.ID) + } + + tags := []*entity.Tag{ + { + SlugName: "go", + DisplayName: "Golang", + OriginalText: "golang", + ParsedText: "

golang

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "java", + DisplayName: "Java", + OriginalText: "java", + ParsedText: "

java

", + Status: entity.TagStatusAvailable, + }, + } + err = tagCommenRepo.AddTagList(context.TODO(), tags) + assert.NoError(t, err) + + tagRels := make([]*entity.TagRel, 0) + for i, question := range questions { + tagRel := &entity.TagRel{ + TagID: tags[i/4].ID, + ObjectID: question.ID, + Status: entity.TagRelStatusAvailable, + } + tagRels = append(tagRels, tagRel) + } + err = tagRelRepo.AddTagRelList(context.TODO(), tagRels) + assert.NoError(t, err) + + followQuestionIDs := make([]string, 0) + for i := range questions { + if i%2 == 0 { + continue + } + err = followRepo.Follow(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + followQuestionIDs = append(followQuestionIDs, questions[i].ID) + } + + // get recommend + questionList, total, err := questionRepo.GetRecommendQuestionPageByTags(context.TODO(), user.ID, []string{tags[0].ID}, followQuestionIDs, 1, 20) + assert.NoError(t, err) + assert.Equal(t, int64(5), total) + assert.Equal(t, 5, len(questionList)) + + // recovery + t.Cleanup(func() { + tagRelIDs := make([]int64, 0) + for i, tagRel := range tagRels { + if i%2 == 1 { + err = followRepo.FollowCancel(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + } + tagRelIDs = append(tagRelIDs, tagRel.ID) + } + err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs) + assert.NoError(t, err) + for _, tag := range tags { + err = tagRepo.RemoveTag(context.TODO(), tag.ID) + assert.NoError(t, err) + } + for _, q := range questions { + err = questionRepo.RemoveQuestion(context.TODO(), q.ID) + assert.NoError(t, err) + } + }) +} diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go new file mode 100644 index 000000000..a59ae6269 --- /dev/null +++ b/internal/repo/repo_test/repo_main_test.go @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "database/sql" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/migrations" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/segmentfault/pacman/cache" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +var ( + mysqlDBSetting = TestDBSetting{ + Driver: string(schemas.MYSQL), + ImageName: "mariadb", + ImageVersion: "10.4.7", + ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"}, + PortID: "3306/tcp", + Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id + } + postgresDBSetting = TestDBSetting{ + Driver: string(schemas.POSTGRES), + ImageName: "postgres", + ImageVersion: "14", + ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"}, + PortID: "5432/tcp", + Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable", + } + sqlite3DBSetting = TestDBSetting{ + Driver: string(schemas.SQLITE), + Connection: filepath.Join(os.TempDir(), "answer-test-data.db"), + } + dbSettingMapping = map[string]TestDBSetting{ + mysqlDBSetting.Driver: mysqlDBSetting, + sqlite3DBSetting.Driver: sqlite3DBSetting, + postgresDBSetting.Driver: postgresDBSetting, + } + // after all test down will execute tearDown function to clean-up + tearDown func() + // testDataSource used for repo testing + testDataSource *data.Data +) + +func TestMain(t *testing.M) { + dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")] + if !ok { + // Use sqlite3 to test. + dbSetting = dbSettingMapping[string(schemas.SQLITE)] + } + if dbSetting.Driver == string(schemas.SQLITE) { + os.RemoveAll(dbSetting.Connection) + } + + defer func() { + if tearDown != nil { + tearDown() + } + }() + if err := initTestDataSource(dbSetting); err != nil { + panic(err) + } + log.Info("init test database successfully") + + if ret := t.Run(); ret != 0 { + panic(ret) + } +} + +type TestDBSetting struct { + Driver string + ImageName string + ImageVersion string + ENV []string + PortID string + Connection string +} + +func initTestDataSource(dbSetting TestDBSetting) error { + connection, imageCleanUp, err := initDatabaseImage(dbSetting) + if err != nil { + return err + } + dbSetting.Connection = connection + + dbEngine, err := initDatabase(dbSetting) + if err != nil { + return err + } + + newCache, err := initCache() + if err != nil { + return err + } + + newData, dbCleanUp, err := data.NewData(dbEngine, newCache) + if err != nil { + return err + } + testDataSource = newData + + tearDown = func() { + dbCleanUp() + log.Info("cleanup test database successfully") + imageCleanUp() + log.Info("cleanup test database image successfully") + } + return nil +} + +func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { + // sqlite3 don't need to set up image + if dbSetting.Driver == string(schemas.SQLITE) { + return dbSetting.Connection, func() { + log.Info("remove database", dbSetting.Connection) + err = os.Remove(dbSetting.Connection) + if err != nil { + log.Error(err) + } + }, nil + } + pool, err := dockertest.NewPool("") + pool.MaxWait = time.Minute * 5 + if err != nil { + return "", nil, fmt.Errorf("could not connect to docker: %s", err) + } + + //resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV) + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: dbSetting.ImageName, + Tag: dbSetting.ImageVersion, + Env: dbSetting.ENV, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + return "", nil, fmt.Errorf("could not pull resource: %s", err) + } + + connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) + if err := pool.Retry(func() error { + db, err := sql.Open(dbSetting.Driver, connection) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + return "", nil, fmt.Errorf("could not connect to database: %s", err) + } + return connection, func() { _ = pool.Purge(resource) }, nil +} + +func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { + dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} + dbEngine, err = data.NewDB(true, dataConf) + if err != nil { + return nil, fmt.Errorf("connection to database failed: %s", err) + } + if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{ + Language: "en_US", + SiteName: "ANSWER", + SiteURL: "http://127.0.0.1:8080/", + ContactEmail: "answer@answer.com", + AdminName: "admin", + AdminPassword: "admin", + AdminEmail: "answer@answer.com", + }).InitDB(); err != nil { + return nil, fmt.Errorf("migrations init database failed: %s", err) + } + return dbEngine, nil +} + +func initCache() (newCache cache.Cache, err error) { + newCache, _, err = data.NewCache(&data.CacheConf{}) + return newCache, err +} diff --git a/internal/repo/repo_test/revision_repo_test.go b/internal/repo/repo_test/revision_repo_test.go new file mode 100644 index 000000000..01d262d2b --- /dev/null +++ b/internal/repo/repo_test/revision_repo_test.go @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/question" + "github.com/apache/answer/internal/repo/revision" + "github.com/apache/answer/internal/repo/unique" + "github.com/stretchr/testify/assert" +) + +var q = &entity.Question{ + ID: "", + UserID: "1", + Title: "test", + OriginalText: "test", + ParsedText: "test", + Status: entity.QuestionStatusAvailable, + ViewCount: 0, + UniqueViewCount: 0, + VoteCount: 0, + AnswerCount: 0, + CollectionCount: 0, + FollowCount: 0, + AcceptedAnswerID: "", + LastAnswerID: "", + RevisionID: "0", +} + +func getRev(objID, title, content string) *entity.Revision { + return &entity.Revision{ + ID: "", + UserID: "1", + ObjectID: objID, + Title: title, + Content: content, + Log: "add rev", + } +} + +func Test_revisionRepo_AddRevision(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) + ) + + // create question + err := questionRepo.AddQuestion(context.TODO(), q) + assert.NoError(t, err) + assert.NotEqual(t, "", q.ID) + + content, err := json.Marshal(q) + assert.NoError(t, err) + // auto update false + rev := getRev(q.ID, q.Title, string(content)) + err = revisionRepo.AddRevision(context.TODO(), rev, false) + assert.NoError(t, err) + qr, _, _ := questionRepo.GetQuestion(context.TODO(), q.ID) + assert.NotEqual(t, rev.ID, qr.RevisionID) + + // auto update false + rev = getRev(q.ID, q.Title, string(content)) + err = revisionRepo.AddRevision(context.TODO(), rev, true) + assert.NoError(t, err) + qr, _, _ = questionRepo.GetQuestion(context.TODO(), q.ID) + assert.Equal(t, rev.ID, qr.RevisionID) + + // recovery + t.Cleanup(func() { + err = questionRepo.RemoveQuestion(context.TODO(), q.ID) + assert.NoError(t, err) + }) +} + +func Test_revisionRepo_GetLastRevisionByObjectID(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + ) + + Test_revisionRepo_AddRevision(t) + rev, exists, err := revisionRepo.GetLastRevisionByObjectID(context.TODO(), q.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.NotNil(t, rev) +} + +func Test_revisionRepo_GetRevisionList(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + ) + Test_revisionRepo_AddRevision(t) + revs, err := revisionRepo.GetRevisionList(context.TODO(), &entity.Revision{ObjectID: q.ID}) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(revs), 1) +} diff --git a/internal/repo/repo_test/siteinfo_repo_test.go b/internal/repo/repo_test/siteinfo_repo_test.go new file mode 100644 index 000000000..cb5d8fc0d --- /dev/null +++ b/internal/repo/repo_test/siteinfo_repo_test.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/site_info" + "github.com/stretchr/testify/assert" +) + +func Test_siteInfoRepo_SaveByType(t *testing.T) { + siteInfoRepo := site_info.NewSiteInfo(testDataSource) + + data := &entity.SiteInfo{Content: "site_info", Type: "test"} + + err := siteInfoRepo.SaveByType(context.TODO(), data.Type, data) + assert.NoError(t, err) + + got, exist, err := siteInfoRepo.GetByType(context.TODO(), data.Type) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, data.Content, got.Content) + + data.Content = "new site_info" + err = siteInfoRepo.SaveByType(context.TODO(), data.Type, data) + assert.NoError(t, err) + + got, exist, err = siteInfoRepo.GetByType(context.TODO(), data.Type) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, data.Content, got.Content) +} diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go new file mode 100644 index 000000000..e3af67a14 --- /dev/null +++ b/internal/repo/repo_test/tag_rel_repo_test.go @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "log" + "sync" + "testing" + + "github.com/apache/answer/internal/repo/unique" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/tag" + "github.com/stretchr/testify/assert" +) + +var ( + tagRelOnce sync.Once + testTagRelList = []*entity.TagRel{ + { + ObjectID: "10010000000000101", + TagID: "10030000000000101", + Status: entity.TagRelStatusAvailable, + }, + { + ObjectID: "10010000000000202", + TagID: "10030000000000202", + Status: entity.TagRelStatusAvailable, + }, + } +) + +func addTagRelList() { + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + err := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList) + if err != nil { + log.Fatalf("%+v", err) + } +} + +func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + relList, err := + tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) + assert.NoError(t, err) + assert.Equal(t, 2, len(relList)) +} + +func Test_tagListRepo_CountTagRelByTagID(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") + assert.NoError(t, err) + assert.Equal(t, int64(1), count) +} + +func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + relList, err := + tagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID) + assert.NoError(t, err) + assert.Equal(t, 1, len(relList)) +} + +func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + relList, err := + tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) + assert.NoError(t, err) + assert.Equal(t, 2, len(relList)) + + ids := []int64{relList[0].ID, relList[1].ID} + err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids) + assert.NoError(t, err) + + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") + assert.NoError(t, err) + assert.Equal(t, int64(0), count) + + _, exist, err := tagRelRepo.GetObjectTagRelWithoutStatus(context.TODO(), relList[0].ObjectID, relList[0].TagID) + assert.NoError(t, err) + assert.True(t, exist) + + err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids, false) + assert.NoError(t, err) + + count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101") + assert.NoError(t, err) + assert.Equal(t, int64(1), count) +} diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go new file mode 100644 index 000000000..b2475a227 --- /dev/null +++ b/internal/repo/repo_test/tag_repo_test.go @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "fmt" + "log" + "sync" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/tag" + "github.com/apache/answer/internal/repo/tag_common" + "github.com/apache/answer/internal/repo/unique" + "github.com/apache/answer/pkg/converter" + "github.com/stretchr/testify/assert" +) + +var ( + tagOnce sync.Once + testTagList = []*entity.Tag{ + { + SlugName: "go", + DisplayName: "Golang", + OriginalText: "golang", + ParsedText: "

golang

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "js", + DisplayName: "javascript", + OriginalText: "javascript", + ParsedText: "

javascript

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "go2", + DisplayName: "Golang2", + OriginalText: "golang2", + ParsedText: "

golang2

", + Status: entity.TagStatusAvailable, + }, + } +) + +func addTagList() { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) + err := tagCommonRepo.AddTagList(context.TODO(), testTagList) + if err != nil { + log.Fatalf("%+v", err) + } +} + +func Test_tagRepo_GetTagByID(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) +} + +func Test_tagRepo_GetTagBySlugName(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) +} + +func Test_tagRepo_GetTagList(t *testing.T) { + tagOnce.Do(addTagList) + tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagRepo.GetTagList(context.TODO(), &entity.Tag{ID: testTagList[0].ID}) + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_GetTagListByIDs(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID}) + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_GetTagListByName(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false, false) + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_GetTagListByNames(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName}) + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_GetTagPage(t *testing.T) { + tagOnce.Do(addTagList) + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "") + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_RemoveTag(t *testing.T) { + tagOnce.Do(addTagList) + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID) + assert.NoError(t, err) + + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + _, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName) + assert.NoError(t, err) + assert.False(t, exist) +} + +func Test_tagRepo_UpdateTag(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + + testTagList[0].DisplayName = "golang" + err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) + assert.NoError(t, err) + + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName) +} + +func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) { + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + testTagList[0].DisplayName = "golang" + err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100) + assert.NoError(t, err) + + gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 100, gotTag.QuestionCount) +} + +func Test_tagRepo_UpdateTagSynonym(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + + testTagList[0].DisplayName = "golang" + err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) + assert.NoError(t, err) + + err = tagRepo.UpdateTagSynonym(context.TODO(), []string{testTagList[2].SlugName}, + converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName) + assert.NoError(t, err) + + tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID)) +} diff --git a/internal/repo/repo_test/user_backyard_repo_test.go b/internal/repo/repo_test/user_backyard_repo_test.go new file mode 100644 index 000000000..e1db0d601 --- /dev/null +++ b/internal/repo/repo_test/user_backyard_repo_test.go @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/auth" + "github.com/apache/answer/internal/repo/user" + "github.com/stretchr/testify/assert" +) + +func Test_userAdminRepo_GetUserInfo(t *testing.T) { + userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "1", got.ID) +} + +func Test_userAdminRepo_GetUserPage(t *testing.T) { + userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, total, err := userAdminRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false) + assert.NoError(t, err) + assert.Equal(t, int64(1), total) + assert.Equal(t, "1", got[0].ID) +} + +func Test_userAdminRepo_UpdateUserStatus(t *testing.T) { + userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusAvailable, got.Status) + + err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable, + "admin@admin.com") + assert.NoError(t, err) + + got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusSuspended, got.Status) + + err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable, + "admin@admin.com") + assert.NoError(t, err) + + got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusAvailable, got.Status) +} diff --git a/internal/repo/repo_test/user_repo_test.go b/internal/repo/repo_test/user_repo_test.go new file mode 100644 index 000000000..7b18833dd --- /dev/null +++ b/internal/repo/repo_test/user_repo_test.go @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo_test + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/repo/user" + "github.com/stretchr/testify/assert" +) + +func Test_userRepo_AddUser(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + userInfo := &entity.User{ + Username: "answer", + Pass: "answer", + EMail: "answer@example.com", + MailStatus: entity.EmailStatusAvailable, + Status: entity.UserStatusAvailable, + DisplayName: "answer", + IsAdmin: false, + } + err := userRepo.AddUser(context.TODO(), userInfo) + assert.NoError(t, err) +} + +func Test_userRepo_BatchGetByID(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + got, err := userRepo.BatchGetByID(context.TODO(), []string{"1"}) + assert.NoError(t, err) + assert.Equal(t, 1, len(got)) + assert.Equal(t, "admin", got[0].Username) +} + +func Test_userRepo_GetByEmail(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + got, exist, err := userRepo.GetByEmail(context.TODO(), "admin@admin.com") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_GetByUserID(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_GetByUsername(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + got, exist, err := userRepo.GetByUsername(context.TODO(), "admin") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_IncreaseAnswerCount(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.IncreaseAnswerCount(context.TODO(), "1", 1) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 1, got.AnswerCount) +} + +func Test_userRepo_IncreaseQuestionCount(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.IncreaseQuestionCount(context.TODO(), "1", 1) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 1, got.AnswerCount) +} + +func Test_userRepo_UpdateEmail(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdateEmail(context.TODO(), "1", "admin@admin.com") + assert.NoError(t, err) +} + +func Test_userRepo_UpdateEmailStatus(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdateEmailStatus(context.TODO(), "1", entity.EmailStatusToBeVerified) + assert.NoError(t, err) +} + +func Test_userRepo_UpdateInfo(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: "1", Bio: "test"}) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "test", got.Bio) +} + +func Test_userRepo_UpdateLastLoginDate(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdateLastLoginDate(context.TODO(), "1") + assert.NoError(t, err) +} + +func Test_userRepo_UpdateNoticeStatus(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdateNoticeStatus(context.TODO(), "1", 1) + assert.NoError(t, err) +} + +func Test_userRepo_UpdatePass(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource) + err := userRepo.UpdatePass(context.TODO(), "1", "admin") + assert.NoError(t, err) +} diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go index bb8246a9f..e597c9a11 100644 --- a/internal/repo/report/report_repo.go +++ b/internal/repo/report/report_repo.go @@ -1,17 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package report import ( "context" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/report_common" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/report_common" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) @@ -35,7 +53,7 @@ func (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err if err != nil { return err } - _, err = rr.data.DB.Insert(report) + _, err = rr.data.DB.Context(ctx).Insert(report) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -43,31 +61,11 @@ func (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err } // GetReportListPage get report list page -func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) { - var ( - ok bool - status int - objectType int - session = rr.data.DB.NewSession() - cond = entity.Report{} - ) - - // parse status - status, ok = entity.ReportStatus[dto.Status] - if !ok { - status = entity.ReportStatus["pending"] - } - cond.Status = status - - // parse object type - objectType, ok = constant.ObjectTypeStrMapping[dto.ObjectType] - if ok { - cond.ObjectType = objectType - } - - // order - session.OrderBy("updated_at desc") - +func (rr *reportRepo) GetReportListPage(ctx context.Context, dto *schema.GetReportListPageDTO) ( + reports []*entity.Report, total int64, err error) { + cond := &entity.Report{} + cond.Status = dto.Status + session := rr.data.DB.Context(ctx).Desc("updated_at") total, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -76,20 +74,29 @@ func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetRepor } // GetByID get report by ID -func (ar *reportRepo) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) { - report = entity.Report{} - exist, err = ar.data.DB.ID(id).Get(&report) +func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) { + report = &entity.Report{} + exist, err = rr.data.DB.Context(ctx).ID(id).Get(report) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } -// UpdateByID handle report by ID -func (ar *reportRepo) UpdateByID( - ctx context.Context, - id string, - handleData entity.Report) (err error) { - _, err = ar.data.DB.ID(id).Update(&handleData) +// UpdateStatus update report status by ID +func (rr *reportRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) { + _, err = rr.data.DB.Context(ctx).ID(id).Update(&entity.Report{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } + +func (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Report, 0) + count, err = rr.data.DB.Context(ctx).Where("status =?", entity.ReportStatusPending).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/review/review_repo.go b/internal/repo/review/review_repo.go new file mode 100644 index 000000000..20cc82aab --- /dev/null +++ b/internal/repo/review/review_repo.go @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package review + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/review" + "github.com/segmentfault/pacman/errors" +) + +// reviewRepo review repository +type reviewRepo struct { + data *data.Data +} + +// NewReviewRepo new repository +func NewReviewRepo(data *data.Data) review.ReviewRepo { + return &reviewRepo{ + data: data, + } +} + +// AddReview add review +func (cr *reviewRepo) AddReview(ctx context.Context, review *entity.Review) (err error) { + _, err = cr.data.DB.Context(ctx).Insert(review) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateReviewStatus update review status +func (cr *reviewRepo) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) { + _, err = cr.data.DB.Context(ctx).ID(reviewID).Update(&entity.Review{ + ReviewerUserID: reviewerUserID, Status: status}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReview get review one +func (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) ( + review *entity.Review, exist bool, err error) { + review = &entity.Review{} + exist, err = cr.data.DB.Context(ctx).ID(reviewID).Get(review) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReviewByObject get review by object +func (cr *reviewRepo) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) { + review = &entity.Review{} + exist, err = cr.data.DB.Context(ctx).Desc("id").Where("object_id = ?", objectID).Get(review) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReviewCount get review count +func (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) { + count, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetReviewPage get review page +func (cr *reviewRepo) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) ( + reviewList []*entity.Review, total int64, err error) { + session := cr.data.DB.Context(ctx).Asc("created_at") + reviewList = make([]*entity.Review, 0) + total, err = pager.Help(page, pageSize, &reviewList, cond, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 4529caedc..09ba1aacd 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -1,15 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package revision import ( "context" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/revision" - "github.com/answerdev/answer/internal/service/unique" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/revision" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" "xorm.io/builder" "xorm.io/xorm" @@ -44,6 +65,7 @@ func (rr *revisionRepo) AddRevision(ctx context.Context, revision *entity.Revisi return nil } _, err = rr.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) { + session = session.Context(ctx) _, err = session.Insert(revision) if err != nil { _ = session.Rollback() @@ -79,11 +101,49 @@ func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *en return nil } +// UpdateStatus update revision status +func (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) { + if id == "" { + return nil + } + var data entity.Revision + data.ID = id + data.Status = status + data.ReviewUserID = converter.StringToInt64(reviewUserID) + _, err = rr.data.DB.Context(ctx).Where("id =?", id).Cols("status", "review_user_id").Update(&data) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + // GetRevision get revision one func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( + revision *entity.Revision, exist bool, err error, +) { + revision = &entity.Revision{} + exist, err = rr.data.DB.Context(ctx).ID(id).Get(revision) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetRevisionByID get object's last revision by object TagID +func (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) ( + revision *entity.Revision, exist bool, err error) { + revision = &entity.Revision{} + exist, err = rr.data.DB.Context(ctx).Where("id = ?", revisionID).Get(revision) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) ( revision *entity.Revision, exist bool, err error) { revision = &entity.Revision{} - exist, err = rr.data.DB.ID(id).Get(revision) + exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.RevisionUnreviewedStatus).Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -92,9 +152,20 @@ func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( // GetLastRevisionByObjectID get object's last revision by object TagID func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) ( - revision *entity.Revision, exist bool, err error) { + revision *entity.Revision, exist bool, err error, +) { + revision = &entity.Revision{} + exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).Desc("created_at").Get(revision) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetLastRevisionByFileURL get object's last revision by file url +func (rr *revisionRepo) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) { revision = &entity.Revision{} - exist, err = rr.data.DB.Where("object_id = ?", objectID).OrderBy("create_time DESC").Get(revision) + exist, err = rr.data.DB.Context(ctx).Where("content LIKE ?", "%"+fileURL+"%").Desc("created_at").Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -104,7 +175,7 @@ func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID // GetRevisionList get revision list all func (rr *revisionRepo) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) { revisionList = []entity.Revision{} - err = rr.data.DB.Where(builder.Eq{ + err = rr.data.DB.Context(ctx).Where(builder.Eq{ "object_id": revision.ObjectID, }).OrderBy("created_at DESC").Find(&revisionList) if err != nil { @@ -126,3 +197,37 @@ func (rr *revisionRepo) allowRecord(objectType int) (ok bool) { return false } } + +// GetUnreviewedRevisionPage get unreviewed revision page +func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int, + objectTypeList []int) (revisionList []*entity.Revision, total int64, err error) { + revisionList = make([]*entity.Revision, 0) + if len(objectTypeList) == 0 { + return revisionList, 0, nil + } + session := rr.data.DB.Context(ctx) + session = session.And("status = ?", entity.RevisionUnreviewedStatus) + session = session.In("object_type", objectTypeList) + session = session.OrderBy("created_at asc") + + total, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// CountUnreviewedRevision get unreviewed revision count +func (rr *revisionRepo) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) { + if len(objectTypeList) == 0 { + return 0, nil + } + session := rr.data.DB.Context(ctx) + session = session.And("status = ?", entity.RevisionUnreviewedStatus) + session = session.In("object_type", objectTypeList) + count, err = session.Count(&entity.Revision{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/role/power_repo.go b/internal/repo/role/power_repo.go new file mode 100644 index 000000000..bbd600e9e --- /dev/null +++ b/internal/repo/role/power_repo.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/role" + "github.com/segmentfault/pacman/errors" +) + +// powerRepo power repository +type powerRepo struct { + data *data.Data +} + +// NewPowerRepo new repository +func NewPowerRepo(data *data.Data) role.PowerRepo { + return &powerRepo{ + data: data, + } +} + +// GetPowerList get list all +func (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) { + powerList = make([]*entity.Power, 0) + err = pr.data.DB.Context(ctx).Find(&powerList, power) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/role/role_power_rel_repo.go b/internal/repo/role/role_power_rel_repo.go new file mode 100644 index 000000000..76a3c76d8 --- /dev/null +++ b/internal/repo/role/role_power_rel_repo.go @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/role" + "github.com/segmentfault/pacman/errors" + "xorm.io/builder" +) + +// rolePowerRelRepo rolePowerRel repository +type rolePowerRelRepo struct { + data *data.Data +} + +// NewRolePowerRelRepo new repository +func NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo { + return &rolePowerRelRepo{ + data: data, + } +} + +// GetRolePowerTypeList get role power type list +func (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) { + powers = make([]string, 0) + err = rr.data.DB.Context(ctx).Table("role_power_rel"). + Cols("power_type").Where(builder.Eq{"role_id": roleID}).Find(&powers) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/role/role_repo.go b/internal/repo/role/role_repo.go new file mode 100644 index 000000000..51dfb2327 --- /dev/null +++ b/internal/repo/role/role_repo.go @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + service "github.com/apache/answer/internal/service/role" + "github.com/segmentfault/pacman/errors" +) + +// roleRepo role repository +type roleRepo struct { + data *data.Data +} + +// NewRoleRepo new repository +func NewRoleRepo(data *data.Data) service.RoleRepo { + return &roleRepo{ + data: data, + } +} + +// GetRoleAllList get role list all +func (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) { + roleList = make([]*entity.Role, 0) + err = rr.data.DB.Context(ctx).Find(&roleList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetRoleAllMapping get role all mapping +func (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) { + roleList, err := rr.GetRoleAllList(ctx) + if err != nil { + return nil, err + } + roleMapping = make(map[int]*entity.Role, 0) + for _, role := range roleList { + roleMapping[role.ID] = role + } + return roleMapping, nil +} diff --git a/internal/repo/role/user_role_rel_repo.go b/internal/repo/role/user_role_rel_repo.go new file mode 100644 index 000000000..7bd14ecea --- /dev/null +++ b/internal/repo/role/user_role_rel_repo.go @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/role" + "github.com/segmentfault/pacman/errors" + "xorm.io/builder" + "xorm.io/xorm" +) + +// userRoleRelRepo userRoleRel repository +type userRoleRelRepo struct { + data *data.Data +} + +// NewUserRoleRelRepo new repository +func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo { + return &userRoleRelRepo{ + data: data, + } +} + +// SaveUserRoleRel save user role rel +func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) { + _, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) { + session = session.Context(ctx) + item := &entity.UserRoleRel{UserID: userID} + exist, err := session.Get(item) + if err != nil { + return nil, err + } + if exist { + item.RoleID = roleID + _, err = session.ID(item.ID).Update(item) + } else { + _, err = session.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID}) + } + if err != nil { + return nil, err + } + return nil, nil + }) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUserRoleRelList get user role all +func (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) ( + userRoleRelList []*entity.UserRoleRel, err error) { + userRoleRelList = make([]*entity.UserRoleRel, 0) + err = ur.data.DB.Context(ctx).In("user_id", userIDs).Find(&userRoleRelList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUserRoleRelListByRoleID get user role all by role id +func (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) ( + userRoleRelList []*entity.UserRoleRel, err error) { + userRoleRelList = make([]*entity.UserRoleRel, 0) + err = ur.data.DB.Context(ctx).In("role_id", roleIDs).Find(&userRoleRelList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUserRoleRel get user role +func (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) ( + rolePowerRel *entity.UserRoleRel, exist bool, err error) { + rolePowerRel = &entity.UserRoleRel{} + exist, err = ur.data.DB.Context(ctx).Where(builder.Eq{"user_id": userID}).Get(rolePowerRel) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go new file mode 100644 index 000000000..314c51878 --- /dev/null +++ b/internal/repo/search_common/search_repo.go @@ -0,0 +1,618 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package search_common + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/plugin" + + "github.com/apache/answer/pkg/htmltext" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/search_common" + "github.com/apache/answer/internal/service/unique" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/errors" + "xorm.io/builder" +) + +var ( + qFields = []string{ + "`question`.`id`", + "`question`.`id` as `question_id`", + "`title`", + "`parsed_text`", + "`question`.`created_at` as `created_at`", + "`user_id`", + "`vote_count`", + "`answer_count`", + "CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`", + "`question`.`status` as `status`", + "`post_update_time`", + } + aFields = []string{ + "`answer`.`id` as `id`", + "`question_id`", + "`question`.`title` as `title`", + "`answer`.`parsed_text` as `parsed_text`", + "`answer`.`created_at` as `created_at`", + "`answer`.`user_id` as `user_id`", + "`answer`.`vote_count` as `vote_count`", + "0 as `answer_count`", + "`adopted` as `accepted`", + "`answer`.`status` as `status`", + "`answer`.`created_at` as `post_update_time`", + } +) + +// searchRepo tag repository +type searchRepo struct { + data *data.Data + userCommon *usercommon.UserCommon + uniqueIDRepo unique.UniqueIDRepo + tagCommon *tagcommon.TagCommonService +} + +// NewSearchRepo new repository +func NewSearchRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, + userCommon *usercommon.UserCommon, + tagCommon *tagcommon.TagCommonService, +) search_common.SearchRepo { + return &searchRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + userCommon: userCommon, + tagCommon: tagCommon, + } +} + +// SearchContents search question and answer data +func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { + words = filterWords(words) + + var ( + b *builder.Builder + ub *builder.Builder + qfs = qFields + afs = aFields + argsQ = []interface{}{} + argsA = []interface{}{} + ) + + if order == "relevance" { + if len(words) > 0 { + qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs) + afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) + } else { + order = "newest" + } + } + + b = builder.MySQL().Select(qfs...).From("`question`") + ub = builder.MySQL().Select(afs...).From("`answer`"). + LeftJoin("`question`", "`question`.id = `answer`.question_id") + + b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}). + And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + + argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow) + argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow) + + likeConQ := builder.NewCond() + likeConA := builder.NewCond() + for _, word := range words { + likeConQ = likeConQ.Or(builder.Like{"title", word}). + Or(builder.Like{"original_text", word}) + argsQ = append(argsQ, "%"+word+"%") + argsQ = append(argsQ, "%"+word+"%") + + likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word}) + argsA = append(argsA, "%"+word+"%") + } + + b.Where(likeConQ) + ub.Where(likeConA) + + // check tag + for ti, tagID := range tagIDs { + ast := "tag_rel" + strconv.Itoa(ti) + b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id"). + And(builder.Eq{ + ast + ".status": entity.TagRelStatusAvailable, + }). + And(builder.In(ast+".tag_id", tagID)) + ub.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id"). + And(builder.Eq{ + ast + ".status": entity.TagRelStatusAvailable, + }). + And(builder.In(ast+".tag_id", tagID)) + argsQ = append(argsQ, entity.TagRelStatusAvailable) + argsA = append(argsA, entity.TagRelStatusAvailable) + for _, t := range tagID { + argsQ = append(argsQ, t) + argsA = append(argsA, t) + } + } + + // check user + if userID != "" { + b.Where(builder.Eq{"question.user_id": userID}) + ub.Where(builder.Eq{"answer.user_id": userID}) + argsQ = append(argsQ, userID) + argsA = append(argsA, userID) + } + + // check vote + if votes == 0 { + b.Where(builder.Eq{"question.vote_count": votes}) + ub.Where(builder.Eq{"answer.vote_count": votes}) + argsQ = append(argsQ, votes) + argsA = append(argsA, votes) + } else if votes > 0 { + b.Where(builder.Gte{"question.vote_count": votes}) + ub.Where(builder.Gte{"answer.vote_count": votes}) + argsQ = append(argsQ, votes) + argsA = append(argsA, votes) + } + + //b = b.Union("all", ub) + ubSQL, _, err := ub.ToSQL() + if err != nil { + return + } + bSQL, _, err := b.ToSQL() + if err != nil { + return + } + sql := fmt.Sprintf("(%s UNION ALL %s)", bSQL, ubSQL) + + countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL() + if err != nil { + return + } + + startNum := (page - 1) * pageSize + querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() + if err != nil { + return + } + + queryArgs := []interface{}{} + countArgs := []interface{}{} + + queryArgs = append(queryArgs, querySQL) + queryArgs = append(queryArgs, argsQ...) + queryArgs = append(queryArgs, argsA...) + + countArgs = append(countArgs, countSQL) + countArgs = append(countArgs, argsQ...) + countArgs = append(countArgs, argsA...) + + res, err := sr.data.DB.Context(ctx).Query(queryArgs...) + if err != nil { + return + } + + tr, err := sr.data.DB.Context(ctx).Query(countArgs...) + if len(tr) != 0 { + total = converter.StringToInt64(string(tr[0]["total"])) + } + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } else { + resp, err = sr.parseResult(ctx, res, words) + return + } +} + +// SearchQuestions search question data +func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { + words = filterWords(words) + var ( + qfs = qFields + args = []interface{}{} + ) + if order == "relevance" { + if len(words) > 0 { + qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs) + } else { + order = "newest" + } + } + + b := builder.MySQL().Select(qfs...).From("question") + + b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow) + + likeConQ := builder.NewCond() + for _, word := range words { + likeConQ = likeConQ.Or(builder.Like{"title", word}). + Or(builder.Like{"original_text", word}) + args = append(args, "%"+word+"%") + args = append(args, "%"+word+"%") + } + b.Where(likeConQ) + + // check tag + for ti, tagID := range tagIDs { + ast := "tag_rel" + strconv.Itoa(ti) + b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id"). + And(builder.Eq{ + ast + ".status": entity.TagRelStatusAvailable, + }). + And(builder.In(ast+".tag_id", tagID)) + args = append(args, entity.TagRelStatusAvailable) + for _, t := range tagID { + args = append(args, t) + } + } + + // check need filter has not accepted + if notAccepted { + b.And(builder.Eq{"accepted_answer_id": 0}) + args = append(args, 0) + } + + // check views + if views > -1 { + b.And(builder.Gte{"view_count": views}) + args = append(args, views) + } + + // check answers + if answers == 0 { + b.And(builder.Eq{"answer_count": answers}) + args = append(args, answers) + } else if answers > 0 { + b.And(builder.Gte{"answer_count": answers}) + args = append(args, answers) + } + + if answers == 0 { + b.And(builder.Eq{"answer_count": 0}) + args = append(args, 0) + } else if answers > 0 { + b.And(builder.Gte{"answer_count": answers}) + args = append(args, answers) + } + + queryArgs := []interface{}{} + countArgs := []interface{}{} + + countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + if err != nil { + return + } + + startNum := (page - 1) * pageSize + querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() + if err != nil { + return + } + queryArgs = append(queryArgs, querySQL) + queryArgs = append(queryArgs, args...) + + countArgs = append(countArgs, countSQL) + countArgs = append(countArgs, args...) + + res, err := sr.data.DB.Context(ctx).Query(queryArgs...) + if err != nil { + return + } + + tr, err := sr.data.DB.Context(ctx).Query(countArgs...) + if err != nil { + return + } + + if len(tr) != 0 { + total = converter.StringToInt64(string(tr[0]["total"])) + } + resp, err = sr.parseResult(ctx, res, words) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// SearchAnswers search answer data +func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) { + words = filterWords(words) + + var ( + afs = aFields + args = []interface{}{} + ) + if order == "relevance" { + if len(words) > 0 { + afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) + } else { + order = "newest" + } + } + + b := builder.MySQL().Select(afs...).From("`answer`"). + LeftJoin("`question`", "`question`.id = `answer`.question_id") + + b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow) + + likeConA := builder.NewCond() + for _, word := range words { + likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word}) + args = append(args, "%"+word+"%") + } + + b.Where(likeConA) + + // check tag + for ti, tagID := range tagIDs { + ast := "tag_rel" + strconv.Itoa(ti) + b.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id"). + And(builder.Eq{ + ast + ".status": entity.TagRelStatusAvailable, + }). + And(builder.In(ast+".tag_id", tagID)) + args = append(args, entity.TagRelStatusAvailable) + for _, t := range tagID { + args = append(args, t) + } + } + + // check limit accepted + if accepted { + b.Where(builder.Eq{"adopted": schema.AnswerAcceptedEnable}) + args = append(args, schema.AnswerAcceptedEnable) + } + + // check question id + if questionID != "" { + b.Where(builder.Eq{"question_id": questionID}) + args = append(args, questionID) + } + + queryArgs := []interface{}{} + countArgs := []interface{}{} + + countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + if err != nil { + return + } + + startNum := (page - 1) * pageSize + querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL() + if err != nil { + return + } + + queryArgs = append(queryArgs, querySQL) + queryArgs = append(queryArgs, args...) + + countArgs = append(countArgs, countSQL) + countArgs = append(countArgs, args...) + + res, err := sr.data.DB.Context(ctx).Query(queryArgs...) + if err != nil { + return + } + + tr, err := sr.data.DB.Context(ctx).Query(countArgs...) + if err != nil { + return + } + + total = converter.StringToInt64(string(tr[0]["total"])) + resp, err = sr.parseResult(ctx, res, words) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) { + switch order { + case "newest": + res = "created_at desc" + case "active": + res = "post_update_time desc" + case "score": + res = "vote_count desc" + case "relevance": + res = "relevance desc" + default: + res = "created_at desc" + } + return +} + +// ParseSearchPluginResult parse search plugin result +func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) { + var ( + qres []map[string][]byte + res = make([]map[string][]byte, 0) + b *builder.Builder + ) + for _, r := range sres { + switch r.Type { + case "question": + b = builder.MySQL().Select(qFields...).From("question").Where(builder.Eq{"id": r.ID}). + And(builder.Lt{"`status`": entity.QuestionStatusDeleted}) + case "answer": + b = builder.MySQL().Select(aFields...).From("answer").LeftJoin("`question`", "`question`.`id` = `answer`.`question_id`"). + Where(builder.Eq{"`answer`.`id`": r.ID}). + And(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). + And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow}) + } + qres, err = sr.data.DB.Context(ctx).Query(b) + if err != nil || len(qres) == 0 { + continue + } + res = append(res, qres[0]) + } + return sr.parseResult(ctx, res, words) +} + +// parseResult parse search result, return the data structure +func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte, words []string) (resp []*schema.SearchResult, err error) { + questionIDs := make([]string, 0) + userIDs := make([]string, 0) + resultList := make([]*schema.SearchResult, 0) + for _, r := range res { + questionIDs = append(questionIDs, string(r["question_id"])) + userIDs = append(userIDs, string(r["user_id"])) + tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local) + + var ID = string(r["id"]) + var QuestionID = string(r["question_id"]) + if handler.GetEnableShortID(ctx) { + ID = uid.EnShortID(ID) + QuestionID = uid.EnShortID(QuestionID) + } + + object := &schema.SearchObject{ + ID: ID, + QuestionID: QuestionID, + Title: string(r["title"]), + UrlTitle: htmltext.UrlTitle(string(r["title"])), + Excerpt: htmltext.FetchMatchedExcerpt(string(r["parsed_text"]), words, "...", 100), + CreatedAtParsed: tp.Unix(), + UserInfo: &schema.SearchObjectUser{ + ID: string(r["user_id"]), + }, + Tags: make([]*schema.TagResp, 0), + VoteCount: converter.StringToInt(string(r["vote_count"])), + Accepted: string(r["accepted"]) == "2", + AnswerCount: converter.StringToInt(string(r["answer_count"])), + } + + objectKey, err := obj.GetObjectTypeStrByObjectID(string(r["id"])) + if err != nil { + continue + } + + switch objectKey { + case "question": + for k, v := range entity.AdminQuestionSearchStatus { + if v == converter.StringToInt(string(r["status"])) { + object.StatusStr = k + break + } + } + case "answer": + for k, v := range entity.AdminAnswerSearchStatus { + if v == converter.StringToInt(string(r["status"])) { + object.StatusStr = k + break + } + } + } + + resultList = append(resultList, &schema.SearchResult{ + ObjectType: objectKey, + Object: object, + }) + } + + tagsMap, err := sr.tagCommon.BatchGetObjectTag(ctx, questionIDs) + if err != nil { + return nil, err + } + userInfoMap, err := sr.userCommon.BatchUserBasicInfoByID(ctx, userIDs) + if err != nil { + return nil, err + } + + for _, item := range resultList { + tags, ok := tagsMap[item.Object.QuestionID] + if ok { + item.Object.Tags = tags + } + if userInfo := userInfoMap[item.Object.UserInfo.ID]; userInfo != nil { + item.Object.UserInfo.Username = userInfo.Username + item.Object.UserInfo.DisplayName = userInfo.DisplayName + item.Object.UserInfo.Rank = userInfo.Rank + item.Object.UserInfo.Status = userInfo.Status + } + } + return resultList, nil +} + +func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { + relevanceRes := []string{} + args = []interface{}{} + + for _, searchField := range searchFields { + var ( + relevance = "(LENGTH(" + searchField + ") - LENGTH(%s))" + replacement = "REPLACE(%s, ?, '')" + replaceField = searchField + replaced string + argsField = []interface{}{} + ) + + res = fields + for i, word := range words { + if i == 0 { + argsField = append(argsField, word) + replaced = fmt.Sprintf(replacement, replaceField) + } else { + argsField = append(argsField, word) + replaced = fmt.Sprintf(replacement, replaced) + } + } + args = append(args, argsField...) + + relevance = fmt.Sprintf(relevance, replaced) + relevanceRes = append(relevanceRes, relevance) + } + + res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance") + return +} + +func filterWords(words []string) (res []string) { + for _, word := range words { + if strings.TrimSpace(word) != "" { + res = append(res, word) + } + } + return +} diff --git a/internal/repo/search_repo.go b/internal/repo/search_repo.go deleted file mode 100644 index abf533606..000000000 --- a/internal/repo/search_repo.go +++ /dev/null @@ -1,485 +0,0 @@ -package repo - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "github.com/answerdev/answer/internal/service/unique" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/answerdev/answer/pkg/converter" - "github.com/answerdev/answer/pkg/obj" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" - "xorm.io/builder" -) - -var ( - q_fields = []string{ - "`question`.`id`", - "`question`.`id` as `question_id`", - "`title`", - "`original_text`", - "`question`.`created_at`", - "`user_id`", - "`vote_count`", - "`answer_count`", - "0 as `accepted`", - "`question`.`status` as `status`", - "`post_update_time`", - } - a_fields = []string{ - "`answer`.`id` as `id`", - "`question_id`", - "`question`.`title` as `title`", - "`answer`.`original_text` as `original_text`", - "`answer`.`created_at`", - "`answer`.`user_id` as `user_id`", - "`answer`.`vote_count` as `vote_count`", - "0 as `answer_count`", - "`adopted` as `accepted`", - "`answer`.`status` as `status`", - "`answer`.`created_at` as `post_update_time`", - } -) - -// searchRepo tag repository -type searchRepo struct { - data *data.Data - userCommon *usercommon.UserCommon - uniqueIDRepo unique.UniqueIDRepo -} - -// NewSearchRepo new repository -func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon *usercommon.UserCommon) search_common.SearchRepo { - return &searchRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - userCommon: userCommon, - } -} - -// SearchContents search question and answer data -func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) { - var ( - b *builder.Builder - ub *builder.Builder - qfs = q_fields - afs = a_fields - argsQ = []interface{}{} - argsA = []interface{}{} - ) - if order == "relevance" { - qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs) - afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) - } - - b = builder.MySQL().Select(qfs...).From("`question`") - ub = builder.MySQL().Select(afs...).From("`answer`"). - LeftJoin("`question`", "`question`.id = `answer`.question_id") - - b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}) - ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). - And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}) - - argsQ = append(argsQ, entity.QuestionStatusDeleted) - argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted) - - for i, word := range words { - if i == 0 { - b.Where(builder.Like{"title", word}). - Or(builder.Like{"original_text", word}) - argsQ = append(argsQ, "%"+word+"%") - argsQ = append(argsQ, "%"+word+"%") - - ub.Where(builder.Like{"`answer`.original_text", word}) - argsA = append(argsA, "%"+word+"%") - } else { - b.Or(builder.Like{"title", word}). - Or(builder.Like{"original_text", word}) - argsQ = append(argsQ, "%"+word+"%") - argsQ = append(argsQ, "%"+word+"%") - - ub.Or(builder.Like{"`answer`.original_text", word}) - argsA = append(argsA, "%"+word+"%") - } - } - - // check tag - if tagID != "" { - b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id"). - Where(builder.Eq{"tag_rel.tag_id": tagID}) - argsQ = append(argsQ, tagID) - } - - // check user - if userID != "" { - b.Where(builder.Eq{"question.user_id": userID}) - ub.Where(builder.Eq{"answer.user_id": userID}) - argsQ = append(argsQ, userID) - argsA = append(argsA, userID) - } - - // check vote - if votes == 0 { - b.Where(builder.Eq{"question.vote_count": votes}) - ub.Where(builder.Eq{"answer.vote_count": votes}) - argsQ = append(argsQ, votes) - argsA = append(argsA, votes) - } else if votes > 0 { - b.Where(builder.Gte{"question.vote_count": votes}) - ub.Where(builder.Gte{"answer.vote_count": votes}) - argsQ = append(argsQ, votes) - argsA = append(argsA, votes) - } - - b = b.Union("all", ub) - - querySql, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() - if err != nil { - return - } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() - if err != nil { - return - } - - queryArgs := []interface{}{} - countArgs := []interface{}{} - - queryArgs = append(queryArgs, querySql) - queryArgs = append(queryArgs, argsQ...) - queryArgs = append(queryArgs, argsA...) - - countArgs = append(countArgs, countSql) - countArgs = append(countArgs, argsQ...) - countArgs = append(countArgs, argsA...) - - res, err := sr.data.DB.Query(queryArgs...) - if err != nil { - return - } - - tr, err := sr.data.DB.Query(countArgs...) - if len(tr) != 0 { - total = converter.StringToInt64(string(tr[0]["total"])) - } - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } else { - resp, err = sr.parseResult(ctx, res) - return - } -} - -// SearchQuestions search question data -func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) { - var ( - qfs = q_fields - args = []interface{}{} - ) - if order == "relevance" { - qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs) - } - - b := builder.MySQL().Select(qfs...).From("question") - - b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}) - args = append(args, entity.QuestionStatusDeleted) - - for i, word := range words { - if i == 0 { - b.Where(builder.Like{"title", word}). - Or(builder.Like{"original_text", word}) - args = append(args, "%"+word+"%") - args = append(args, "%"+word+"%") - } else { - b.Or(builder.Like{"original_text", word}) - args = append(args, "%"+word+"%") - } - } - - // check need filter has not accepted - if limitNoAccepted { - b.And(builder.Eq{"accepted_answer_id": 0}) - args = append(args, 0) - } - - if answers == 0 { - b.And(builder.Eq{"answer_count": 0}) - args = append(args, 0) - } else if answers > 0 { - b.And(builder.Gte{"answer_count": answers}) - args = append(args, answers) - } - - queryArgs := []interface{}{} - countArgs := []interface{}{} - - querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() - if err != nil { - return - } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() - if err != nil { - return - } - queryArgs = append(queryArgs, querySql) - queryArgs = append(queryArgs, args...) - - countArgs = append(countArgs, countSql) - countArgs = append(countArgs, args...) - - res, err := sr.data.DB.Query(queryArgs...) - if err != nil { - return - } - - tr, err := sr.data.DB.Query(countArgs...) - if err != nil { - return - } - - if len(tr) != 0 { - total = converter.StringToInt64(string(tr[0]["total"])) - } - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } - resp, err = sr.parseResult(ctx, res) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// SearchAnswers search answer data -func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) { - var ( - afs = a_fields - args = []interface{}{} - ) - if order == "relevance" { - afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs) - } - - b := builder.MySQL().Select(afs...).From("`answer`"). - LeftJoin("`question`", "`question`.id = `answer`.question_id") - - b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}). - And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}) - args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted) - - for i, word := range words { - if i == 0 { - b.Where(builder.Like{"`answer`.original_text", word}) - args = append(args, "%"+word+"%") - } else { - b.Or(builder.Like{"`answer`.original_text", word}) - args = append(args, "%"+word+"%") - } - } - - if limitAccepted { - b.Where(builder.Eq{"adopted": schema.Answer_Adopted_Enable}) - args = append(args, schema.Answer_Adopted_Enable) - } - - if questionID != "" { - b.Where(builder.Eq{"question_id": questionID}) - args = append(args, questionID) - } - - queryArgs := []interface{}{} - countArgs := []interface{}{} - - querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() - if err != nil { - return - } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() - if err != nil { - return - } - queryArgs = append(queryArgs, querySql) - queryArgs = append(queryArgs, args...) - - countArgs = append(countArgs, countSql) - countArgs = append(countArgs, args...) - - res, err := sr.data.DB.Query(queryArgs...) - if err != nil { - return - } - - tr, err := sr.data.DB.Query(countArgs...) - if err != nil { - return - } - - total = converter.StringToInt64(string(tr[0]["total"])) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } - resp, err = sr.parseResult(ctx, res) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) { - switch order { - case "newest": - res = "created_at desc" - case "active": - res = "post_update_time desc" - case "score": - res = "vote_count desc" - case "relevance": - res = "relevance desc" - default: - res = "created_at desc" - } - return -} - -func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []schema.SearchResp, err error) { - for _, r := range res { - var ( - objectKey, - status string - - tags []schema.TagResp - tagsEntity []entity.Tag - object schema.SearchObject - ) - objectKey, err = obj.GetObjectTypeStrByObjectID(string(r["id"])) - if err != nil { - continue - } - - tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local) - - // get user info - userInfo, _, e := sr.userCommon.GetUserBasicInfoByID(ctx, string(r["user_id"])) - if e != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() - return - } - - // get tags - err = sr.data.DB. - Select("`display_name`,`slug_name`,`main_tag_slug_name`"). - Table("tag"). - Join("INNER", "tag_rel", "tag.id = tag_rel.tag_id"). - Where(builder.Eq{"tag_rel.object_id": r["question_id"]}). - And(builder.Eq{"tag_rel.status": entity.TagRelStatusAvailable}). - Find(&tagsEntity) - - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } - _ = copier.Copy(&tags, tagsEntity) - switch objectKey { - case "question": - for k, v := range entity.CmsQuestionSearchStatus { - if v == converter.StringToInt(string(r["status"])) { - status = k - break - } - } - case "answer": - for k, v := range entity.CmsAnswerSearchStatus { - if v == converter.StringToInt(string(r["status"])) { - status = k - break - } - } - } - - object = schema.SearchObject{ - ID: string(r["id"]), - Title: string(r["title"]), - Excerpt: cutOutParsedText(string(r["original_text"])), - CreatedAtParsed: tp.Unix(), - UserInfo: userInfo, - Tags: tags, - VoteCount: converter.StringToInt(string(r["vote_count"])), - Accepted: string(r["accepted"]) == "2", - AnswerCount: converter.StringToInt(string(r["answer_count"])), - StatusStr: status, - } - resp = append(resp, schema.SearchResp{ - ObjectType: objectKey, - Object: object, - }) - } - return -} - -// userBasicInfoFormat -func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.User) *schema.UserBasicInfo { - return &schema.UserBasicInfo{ - ID: dbinfo.ID, - Username: dbinfo.Username, - Rank: dbinfo.Rank, - DisplayName: dbinfo.DisplayName, - Avatar: dbinfo.Avatar, - Website: dbinfo.Website, - Location: dbinfo.Location, - IpInfo: dbinfo.IPInfo, - } -} - -func cutOutParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} - -func addRelevanceField(search_fields, words, fields []string) (res []string, args []interface{}) { - var relevanceRes = []string{} - args = []interface{}{} - - for _, search_field := range search_fields { - var ( - relevance = "(LENGTH(" + search_field + ") - LENGTH(%s))" - replacement = "REPLACE(%s, ?, '')" - replace_field = search_field - replaced string - argsField = []interface{}{} - ) - - res = fields - for i, word := range words { - if i == 0 { - argsField = append(argsField, word) - replaced = fmt.Sprintf(replacement, replace_field) - } else { - argsField = append(argsField, word) - replaced = fmt.Sprintf(replacement, replaced) - } - } - args = append(args, argsField...) - - relevance = fmt.Sprintf(relevance, replaced) - relevanceRes = append(relevanceRes, relevance) - } - - res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance") - return -} diff --git a/internal/repo/search_sync/search_sync.go b/internal/repo/search_sync/search_sync.go new file mode 100644 index 000000000..5c0adc0f9 --- /dev/null +++ b/internal/repo/search_sync/search_sync.go @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package search_sync + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" +) + +func NewPluginSyncer(data *data.Data) plugin.SearchSyncer { + return &PluginSyncer{data: data} +} + +type PluginSyncer struct { + data *data.Data +} + +func (p *PluginSyncer) GetAnswersPage(ctx context.Context, page, pageSize int) ( + answerList []*plugin.SearchContent, err error) { + answers := make([]*entity.Answer, 0) + startNum := (page - 1) * pageSize + err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&answers) + if err != nil { + return nil, err + } + return p.convertAnswers(ctx, answers) +} + +func (p *PluginSyncer) GetQuestionsPage(ctx context.Context, page, pageSize int) ( + questionList []*plugin.SearchContent, err error) { + questions := make([]*entity.Question, 0) + startNum := (page - 1) * pageSize + err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&questions) + if err != nil { + return nil, err + } + return p.convertQuestions(ctx, questions) +} + +func (p *PluginSyncer) convertAnswers(ctx context.Context, answers []*entity.Answer) ( + answerList []*plugin.SearchContent, err error) { + for _, answer := range answers { + question := &entity.Question{} + exist, err := p.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(question) + if err != nil { + log.Errorf("get question failed %s", err) + continue + } + if !exist { + continue + } + + tagListList := make([]*entity.TagRel, 0) + tags := make([]string, 0) + err = p.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)). + Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList) + if err != nil { + log.Errorf("get tag list failed %s", err) + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + + content := &plugin.SearchContent{ + ObjectID: answer.ID, + Title: question.Title, + Type: constant.AnswerObjectType, + Content: answer.ParsedText, + Answers: 0, + Status: plugin.SearchContentStatus(answer.Status), + Tags: tags, + QuestionID: answer.QuestionID, + UserID: answer.UserID, + Views: int64(question.ViewCount), + Created: answer.CreatedAt.Unix(), + Active: answer.UpdatedAt.Unix(), + Score: int64(answer.VoteCount), + HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable, + } + answerList = append(answerList, content) + } + return answerList, nil +} + +func (p *PluginSyncer) convertQuestions(ctx context.Context, questions []*entity.Question) ( + questionList []*plugin.SearchContent, err error) { + for _, question := range questions { + tagListList := make([]*entity.TagRel, 0) + tags := make([]string, 0) + err := p.data.DB.Context(ctx).Where("object_id = ?", question.ID). + Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList) + if err != nil { + log.Errorf("get tag list failed %s", err) + } + for _, tag := range tagListList { + tags = append(tags, tag.TagID) + } + content := &plugin.SearchContent{ + ObjectID: question.ID, + Title: question.Title, + Type: constant.QuestionObjectType, + Content: question.ParsedText, + Answers: int64(question.AnswerCount), + Status: plugin.SearchContentStatus(question.Status), + Tags: tags, + QuestionID: question.ID, + UserID: question.UserID, + Views: int64(question.ViewCount), + Created: question.CreatedAt.Unix(), + Active: question.UpdatedAt.Unix(), + Score: int64(question.VoteCount), + HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0", + } + questionList = append(questionList, content) + } + return questionList, nil +} diff --git a/internal/repo/site_info/siteinfo_repo.go b/internal/repo/site_info/siteinfo_repo.go new file mode 100644 index 000000000..5f95b7486 --- /dev/null +++ b/internal/repo/site_info/siteinfo_repo.go @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package site_info + +import ( + "context" + "encoding/json" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" +) + +type siteInfoRepo struct { + data *data.Data +} + +func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo { + return &siteInfoRepo{ + data: data, + } +} + +// SaveByType save site setting by type +func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) { + old := &entity.SiteInfo{} + exist, err := sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(old) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + _, err = sr.data.DB.Context(ctx).ID(old.ID).Update(data) + } else { + _, err = sr.data.DB.Context(ctx).Insert(data) + } + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + sr.setCache(ctx, siteType, data) + return +} + +// GetByType get site info by type +func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) { + siteInfo = sr.getCache(ctx, siteType) + if siteInfo != nil { + return siteInfo, true, nil + } + siteInfo = &entity.SiteInfo{} + exist, err = sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(siteInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, false, err + } + if exist { + sr.setCache(ctx, siteType, siteInfo) + } + return +} + +func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) { + siteInfoCache, exist, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType) + if err != nil { + return nil + } + if !exist { + return nil + } + siteInfo = &entity.SiteInfo{} + _ = json.Unmarshal([]byte(siteInfoCache), siteInfo) + return siteInfo +} + +func (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) { + siteInfoCache, _ := json.Marshal(siteInfo) + err := sr.data.Cache.SetString(ctx, + constant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime) + if err != nil { + log.Error(err) + } +} + +func (sr *siteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) { + siteInfo := &entity.SiteInfo{} + count, err := sr.data.DB.Context(ctx). + Table("site_info"). + Where(builder.Eq{"type": "branding"}). + And(builder.Like{"content", "%" + filePath + "%"}). + Count(&siteInfo) + + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + return count > 0, nil +} diff --git a/internal/repo/siteinfo_repo.go b/internal/repo/siteinfo_repo.go deleted file mode 100644 index 579d9bb15..000000000 --- a/internal/repo/siteinfo_repo.go +++ /dev/null @@ -1,54 +0,0 @@ -package repo - -import ( - "context" - - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/siteinfo_common" - "github.com/segmentfault/pacman/errors" - "xorm.io/builder" -) - -type siteInfoRepo struct { - data *data.Data -} - -func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo { - return &siteInfoRepo{ - data: data, - } -} - -// SaveByType save site setting by type -func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) { - var ( - old = &entity.SiteInfo{} - exist bool - ) - exist, err = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old) - if exist { - _, err = sr.data.DB.ID(old.ID).Update(data) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return - } - - _, err = sr.data.DB.Insert(data) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetByType get site info by type -func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) { - siteInfo = &entity.SiteInfo{} - exist, err = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(siteInfo) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index c54bf8c0d..3634c97a7 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -1,39 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package tag import ( "context" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" ) -// tagListRepo tagList repository -type tagListRepo struct { - data *data.Data +// tagRelRepo tag rel repository +type tagRelRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo } -// NewTagListRepo new repository -func NewTagListRepo(data *data.Data) tagcommon.TagRelRepo { - return &tagListRepo{ - data: data, +// NewTagRelRepo new repository +func NewTagRelRepo(data *data.Data, + uniqueIDRepo unique.UniqueIDRepo) tagcommon.TagRelRepo { + return &tagRelRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, } } // AddTagRelList add tag list -func (tr *tagListRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) { - _, err = tr.data.DB.Insert(tagList) +func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) { + for _, item := range tagList { + item.ObjectID = uid.DeShortID(item.ObjectID) + } + _, err = tr.data.DB.Context(ctx).Insert(tagList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if handler.GetEnableShortID(ctx) { + for _, item := range tagList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } + } return } // RemoveTagRelListByObjectID delete tag list -func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectId string) (err error) { - _, err = tr.data.DB.Where("object_id = ?", objectId).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) +func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + objectID = uid.DeShortID(objectID) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// RecoverTagRelListByObjectID recover tag list +func (tr *tagRelRepo) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + objectID = uid.DeShortID(objectID) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + objectID = uid.DeShortID(objectID) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusAvailable).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + objectID = uid.DeShortID(objectID) + _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusHide).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -41,8 +104,8 @@ func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectId } // RemoveTagRelListByIDs delete tag list -func (tr *tagListRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) { - _, err = tr.data.DB.In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) +func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) { + _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -50,20 +113,30 @@ func (tr *tagListRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) ( } // GetObjectTagRelWithoutStatus get object tag relation no matter status -func (tr *tagListRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) ( - tagRel *entity.TagRel, exist bool, err error) { +func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) ( + tagRel *entity.TagRel, exist bool, err error, +) { + objectID = uid.DeShortID(objectID) tagRel = &entity.TagRel{} - session := tr.data.DB.Where("object_id = ?", objectId).And("tag_id = ?", tagID) + session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("tag_id = ?", tagID) exist, err = session.Get(tagRel) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if handler.GetEnableShortID(ctx) { + tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID) } return } // EnableTagRelByIDs update tag status to available -func (tr *tagListRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) { - _, err = tr.data.DB.In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) +func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) { + status := entity.TagRelStatusAvailable + if hide { + status = entity.TagRelStatusHide + } + _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: status}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -71,34 +144,117 @@ func (tr *tagListRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err } // GetObjectTagRelList get object tag relation list all -func (tr *tagListRepo) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) { +func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) { + objectID = uid.DeShortID(objectID) tagListList = make([]*entity.TagRel, 0) - session := tr.data.DB.Where("object_id = ?", objectId) - session.Where("status = ?", entity.TagRelStatusAvailable) + session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID) + session.In("status", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide}) err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if handler.GetEnableShortID(ctx) { + for _, item := range tagListList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } } return } // BatchGetObjectTagRelList get object tag relation list all -func (tr *tagListRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) { +func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) { + for num, item := range objectIds { + objectIds[num] = uid.DeShortID(item) + } tagListList = make([]*entity.TagRel, 0) - session := tr.data.DB.In("object_id", objectIds) + session := tr.data.DB.Context(ctx).In("object_id", objectIds) session.Where("status = ?", entity.TagRelStatusAvailable) err = session.Find(&tagListList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if handler.GetEnableShortID(ctx) { + for _, item := range tagListList { + item.ObjectID = uid.EnShortID(item.ObjectID) + } } return } // CountTagRelByTagID count tag relation -func (tr *tagListRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { - count, err = tr.data.DB.Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable}) +func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { + count, err = tr.data.DB.Context(ctx).Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } + +// GetTagRelDefaultStatusByObjectID get tag rel default status +func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) { + question := entity.Question{} + exist, err := tr.data.DB.Context(ctx).ID(objectID).Cols("show", "status").Get(&question) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist && (question.Show == entity.QuestionHide || question.Status == entity.QuestionStatusDeleted) { + return entity.TagRelStatusHide, nil + } + return entity.TagRelStatusAvailable, nil +} + +// MigrateTagObjects migrate tag objects +func (tr *tagRelRepo) MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error { + _, err := tr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + // 1. Get all objects related to source tag + var sourceObjects []entity.TagRel + err = session.Where("tag_id = ?", sourceTagId).Find(&sourceObjects) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // 2. Get existing target tag relations + var existingTargets []entity.TagRel + err = session.Where("tag_id = ?", targetTagId).Find(&existingTargets) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // Create map of existing target objects for quick lookup + existingMap := make(map[string]bool) + for _, target := range existingTargets { + existingMap[target.ObjectID] = true + } + + // 3. Create new relations for objects not already tagged with target + newRelations := make([]*entity.TagRel, 0) + for _, source := range sourceObjects { + if !existingMap[source.ObjectID] { + newRelations = append(newRelations, &entity.TagRel{ + TagID: targetTagId, + ObjectID: source.ObjectID, + Status: source.Status, + }) + } + } + + if len(newRelations) > 0 { + _, err = session.Insert(newRelations) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + } + + // 4. Remove old relations + _, err = session.Where("tag_id = ?", sourceTagId).Delete(&entity.TagRel{}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + return nil, nil + }) + + return err +} diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 5243402e5..bdfe62cfa 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -1,15 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package tag import ( "context" - "fmt" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/internal/service/unique" + "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "xorm.io/builder" ) @@ -24,113 +42,68 @@ type tagRepo struct { func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, -) tagcommon.TagRepo { +) tag_common.TagRepo { return &tagRepo{ data: data, uniqueIDRepo: uniqueIDRepo, } } -// AddTagList add tag -func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { - for _, item := range tagList { - ID, err := tr.uniqueIDRepo.GenUniqueID(ctx, item.TableName()) - if err != nil { - return err - } - item.RevisionID = "0" - item.ID = fmt.Sprintf("%d", ID) - } - _, err = tr.data.DB.Insert(tagList) +// RemoveTag delete tag +func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) { + session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}) + _, err = session.Update(&entity.Tag{Status: entity.TagStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// GetTagListByIDs get tag list all -func (tr *tagRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { - tagList = make([]*entity.Tag, 0) - session := tr.data.DB.In("id", ids) - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - err = session.Find(&tagList) +// UpdateTag update tag +func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) { + _, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tag.ID}).Update(tag) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// GetTagBySlugName get tag by slug name -func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) { - tagInfo = &entity.Tag{} - session := tr.data.DB.Where("slug_name = ?", slugName) - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - exist, err = session.Get(tagInfo) - if err != nil { - return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// GetTagListByName get tag list all like name -func (tr *tagRepo) GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error) { - tagList = make([]*entity.Tag, 0) - session := tr.data.DB.Where("slug_name LIKE ?", name+"%") - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - session.Limit(limit).Asc("slug_name") - err = session.Find(&tagList) +// RecoverTag recover deleted tag +func (tr *tagRepo) RecoverTag(ctx context.Context, tagID string) (err error) { + _, err = tr.data.DB.Context(ctx).ID(tagID).Update(&entity.Tag{Status: entity.TagStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// GetTagListByNames get tag list all like name -func (tr *tagRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) { - tagList = make([]*entity.Tag, 0) - session := tr.data.DB.In("slug_name", names) - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - err = session.Find(&tagList) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() +// MustGetTagByNameOrID get tag by name or id +func (tr *tagRepo) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) ( + tag *entity.Tag, exist bool, err error) { + if len(tagID) == 0 && len(slugName) == 0 { + return nil, false, nil } - return -} - -// RemoveTag delete tag -func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) { - session := tr.data.DB.Where(builder.Eq{"id": tagID}) - _, err = session.Update(&entity.Tag{Status: entity.TagStatusDeleted}) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + tag = &entity.Tag{} + session := tr.data.DB.Context(ctx) + if len(tagID) > 0 { + session.ID(tagID) } - return -} - -// UpdateTag update tag -func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) { - _, err = tr.data.DB.Where(builder.Eq{"id": tag.ID}).Update(tag) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + if len(slugName) > 0 { + session.Where(builder.Eq{"slug_name": slugName}) } - return -} - -// UpdateTagQuestionCount update tag question count -func (tr *tagRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) { - cond := &entity.Tag{QuestionCount: questionCount} - _, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond) + exist, err = session.Get(tag) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } // UpdateTagSynonym update synonym tag func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, - mainTagSlugName string) (err error) { + mainTagSlugName string, +) (err error) { bean := &entity.Tag{MainTagID: mainTagID, MainTagSlugName: mainTagSlugName} - session := tr.data.DB.In("slug_name", tagSlugNameList).MustCols("main_tag_id", "main_tag_slug_name") + session := tr.data.DB.Context(ctx).In("slug_name", tagSlugNameList).MustCols("main_tag_id", "main_tag_slug_name") _, err = session.Update(bean) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -138,53 +111,28 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin return } -// GetTagByID get tag one -func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) ( - tag *entity.Tag, exist bool, err error) { - tag = &entity.Tag{} - session := tr.data.DB.Where(builder.Eq{"id": tagID}) - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - exist, err = session.Get(tag) +func (tr *tagRepo) GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) { + count, err = tr.data.DB.Context(ctx).Count(&entity.Tag{MainTagID: converter.StringToInt64(tagID), Status: entity.TagStatusAvailable}) if err != nil { - return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// GetTagList get tag list all -func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) { - tagList = make([]*entity.Tag, 0) - session := tr.data.DB.Where(builder.Eq{"status": entity.TagStatusAvailable}) - err = session.Find(&tagList, tag) +func (tr *tagRepo) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) { + session := tr.data.DB.Context(ctx).Table(entity.Tag{}.TableName()).Where(builder.Eq{"status": entity.TagStatusAvailable, "main_tag_id": converter.StringToInt64(mainTagID)}).Cols("id") + err = session.Find(&tagIDs) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -// GetTagPage get tag page -func (tr *tagRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) ( - tagList []*entity.Tag, total int64, err error) { +// GetTagList get tag list all +func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) - session := tr.data.DB.NewSession() - - if len(tag.SlugName) > 0 { - session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName})) - tag.SlugName = "" - } - session.Where(builder.Eq{"status": entity.TagStatusAvailable}) - session.Where("main_tag_id = 0") // if this tag is synonym, exclude it - - switch queryCond { - case "popular": - session.Desc("question_count") - case "name": - session.Asc("slug_name") - case "newest": - session.Desc("created_at") - } - - total, err = pager.Help(page, pageSize, &tagList, tag, session) + session := tr.data.DB.Context(ctx).Where(builder.Eq{"status": entity.TagStatusAvailable}) + err = session.Find(&tagList, tag) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go new file mode 100644 index 000000000..c4762b0ca --- /dev/null +++ b/internal/repo/tag_common/tag_common_repo.go @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tag_common + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" + "xorm.io/builder" +) + +// tagCommonRepo tag repository +type tagCommonRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +// NewTagCommonRepo new repository +func NewTagCommonRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, +) tagcommon.TagCommonRepo { + return &tagCommonRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +// GetTagListByIDs get tag list all +func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { + tagList = make([]*entity.Tag, 0) + session := tr.data.DB.Context(ctx).In("id", ids) + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetTagBySlugName get tag by slug name +func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) { + tagInfo = &entity.Tag{} + session := tr.data.DB.Context(ctx).Where("LOWER(slug_name) = ?", slugName) + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + exist, err = session.Get(tagInfo) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetTagListByName get tag list all like name +func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) { + cond := &entity.Tag{} + session := tr.data.DB.Context(ctx) + if len(name) > 0 { + session.Where("slug_name LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%") + } + var columns []string + if recommend { + columns = append(columns, "recommend") + cond.Recommend = true + } + if reserved { + columns = append(columns, "reserved") + cond.Reserved = true + } + if len(columns) > 0 { + session.UseBool(columns...) + } + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + + tagList = make([]*entity.Tag, 0) + err = session.OrderBy("recommend DESC,reserved DESC,slug_name ASC").Find(&tagList, cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) { + tagList = make([]*entity.Tag, 0) + cond := &entity.Tag{} + session := tr.data.DB.Context(ctx).Where("") + cond.Recommend = true + // session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + session.Asc("slug_name") + session.UseBool("recommend") + err = session.Find(&tagList, cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) { + tagList = make([]*entity.Tag, 0) + cond := &entity.Tag{} + session := tr.data.DB.Context(ctx).Where("") + cond.Reserved = true + // session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + session.Asc("slug_name") + session.UseBool("reserved") + err = session.Find(&tagList, cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetTagListByNames get tag list all like name +func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) { + tagList = make([]*entity.Tag, 0) + session := tr.data.DB.Context(ctx).In("slug_name", names).UseBool("recommend", "reserved") + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetTagByID get tag one +func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) ( + tag *entity.Tag, exist bool, err error, +) { + tag = &entity.Tag{} + session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}) + if !includeDeleted { + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + } + exist, err = session.Get(tag) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetTagPage get tag page +func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) ( + tagList []*entity.Tag, total int64, err error, +) { + tagList = make([]*entity.Tag, 0) + session := tr.data.DB.Context(ctx) + + if len(tag.SlugName) > 0 { + mainTagCond := builder.And( + builder.Or( + builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)}, + builder.Like{"display_name", tag.SlugName}, + ), + builder.Eq{"main_tag_id": 0}, + ) + synonymCond := builder.And( + builder.Eq{"slug_name": tag.SlugName}, + builder.Neq{"main_tag_id": 0}, + ) + session.Where(builder.Or(mainTagCond, synonymCond)) + tag.SlugName = "" + } else { + session.Where(builder.Eq{"main_tag_id": 0}) + } + session.Where(builder.Eq{"status": entity.TagStatusAvailable}) + + switch queryCond { + case "popular": + session.Desc("question_count") + case "name": + session.Asc("slug_name") + case "newest": + session.Desc("created_at") + } + + total, err = pager.Help(page, pageSize, &tagList, tag, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + + for i := 0; i < len(tagList); i++ { + if tagList[i].MainTagID != 0 { + mainTag, exist, errSynonym := tr.GetTagByID(ctx, strconv.FormatInt(tagList[i].MainTagID, 10), false) + if errSynonym != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(errSynonym).WithStack() + return + } + if exist { + tagList[i] = mainTag + } + } + } + + return +} + +// AddTagList add tag +func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { + addTags := make([]*entity.Tag, 0) + for _, item := range tagList { + exist, err := tr.updateDeletedTag(ctx, item) + if err != nil { + return err + } + if exist { + continue + } + addTags = append(addTags, item) + item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName()) + if err != nil { + return err + } + item.RevisionID = "0" + } + if len(addTags) == 0 { + return nil + } + _, err = tr.data.DB.Context(ctx).Insert(addTags) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagCommonRepo) updateDeletedTag(ctx context.Context, tag *entity.Tag) (exist bool, err error) { + old := &entity.Tag{SlugName: tag.SlugName} + exist, err = tr.data.DB.Context(ctx).Get(old) + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist || old.Status != entity.TagStatusDeleted { + return false, nil + } + tag.ID = old.ID + tag.Status = entity.TagStatusAvailable + tag.RevisionID = "0" + if _, err = tr.data.DB.Context(ctx).ID(tag.ID).Update(tag); err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return true, nil +} + +// UpdateTagQuestionCount update tag question count +func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) { + cond := &entity.Tag{QuestionCount: questionCount} + _, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) { + bean := &entity.Tag{} + switch attribute { + case "recommend": + bean.Recommend = value + case "reserved": + bean.Reserved = value + default: + return + } + session := tr.data.DB.Context(ctx).In("slug_name", tags).Cols(attribute).UseBool(attribute) + _, err = session.Update(bean) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/unique/uniqid_repo.go b/internal/repo/unique/uniqid_repo.go index 44a9c0faf..8ad3e3be8 100644 --- a/internal/repo/unique/uniqid_repo.go +++ b/internal/repo/unique/uniqid_repo.go @@ -1,15 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package unique import ( "context" "fmt" - "strconv" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/unique" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) @@ -25,25 +43,12 @@ func NewUniqueIDRepo(data *data.Data) unique.UniqueIDRepo { } } -// GenUniqueID generate unique id -// 1 + 00x(objectType) + 000000000000x(id) -func (ur *uniqueIDRepo) GenUniqueID(ctx context.Context, key string) (uniqueID int64, err error) { - idStr, err := ur.GenUniqueIDStr(ctx, key) - if err != nil { - return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - uniqueID, err = strconv.ParseInt(idStr, 10, 64) - if err != nil { - return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return uniqueID, nil -} - // GenUniqueIDStr generate unique id string +// 1 + 00x(objectType) + 000000000000x(id) func (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) { objectType := constant.ObjectTypeStrMapping[key] bean := &entity.Uniqid{UniqidType: objectType} - _, err = ur.data.DB.Insert(bean) + _, err = ur.data.DB.Context(ctx).Insert(bean) if err != nil { return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 06ce7d2f0..c93845e97 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package user import ( @@ -5,39 +24,48 @@ import ( "encoding/json" "time" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/user_backyard" + "xorm.io/builder" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/user_admin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) -// userBackyardRepo user repository -type userBackyardRepo struct { - data *data.Data +// userAdminRepo user repository +type userAdminRepo struct { + data *data.Data + authRepo auth.AuthRepo } -// NewUserBackyardRepo new repository -func NewUserBackyardRepo(data *data.Data) user_backyard.UserBackyardRepo { - return &userBackyardRepo{ - data: data, +// NewUserAdminRepo new repository +func NewUserAdminRepo(data *data.Data, authRepo auth.AuthRepo) user_admin.UserAdminRepo { + return &userAdminRepo{ + data: data, + authRepo: authRepo, } } // UpdateUserStatus update user status -func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, - email string) (err error) { +func (ur *userAdminRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, + email string, suspendedUntil time.Time, +) (err error) { cond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email} switch userStatus { case entity.UserStatusSuspended: cond.SuspendedAt = time.Now() + cond.SuspendedUntil = suspendedUntil case entity.UserStatusDeleted: cond.DeletedAt = time.Now() + case entity.UserStatusAvailable: + // When restoring user status, clear suspended until time to zero + cond.SuspendedUntil = time.Time{} } - _, err = ur.data.DB.ID(userID).Update(cond) + _, err = ur.data.DB.Context(ctx).ID(userID).MustCols("status", "mail_status", "e_mail", "suspended_at", "suspended_until", "deleted_at").Update(cond) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -49,8 +77,34 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, } t, _ := json.Marshal(userCacheInfo) log.Infof("user change status: %s", string(t)) - err = ur.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID, string(t), - constant.UserStatusChangedCacheTime) + err = ur.authRepo.SetUserStatus(ctx, userID, userCacheInfo) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// AddUser add user +func (ur *userAdminRepo) AddUser(ctx context.Context, user *entity.User) (err error) { + _, err = ur.data.DB.Context(ctx).Insert(user) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// AddUsers add users +func (ur *userAdminRepo) AddUsers(ctx context.Context, users []*entity.User) (err error) { + _, err = ur.data.DB.Context(ctx).Insert(users) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateUserPassword update user password +func (ur *userAdminRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) { + _, err = ur.data.DB.Context(ctx).ID(userID).Update(&entity.User{Pass: password}) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -58,29 +112,96 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, } // GetUserInfo get user info -func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) { +func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) { user = &entity.User{} - exist, err = ur.data.DB.ID(userID).Get(user) + exist, err = ur.data.DB.Context(ctx).ID(userID).Get(user) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !exist { + return + } + err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user) + if err != nil { + return nil, false, err + } + return +} + +// GetUserInfoByEmail get user info +func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) { + userInfo := &entity.User{} + exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email). + Where("status != ?", entity.UserStatusDeleted).Get(userInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if !exist { + return + } + err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user) + if err != nil { + return nil, false, err + } return } // GetUserPage get user page -func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User) (users []*entity.User, total int64, err error) { +func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, + usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) { users = make([]*entity.User, 0) - session := ur.data.DB.NewSession() - if user.Status == entity.UserStatusDeleted { - session.Desc("deleted_at") - } else if user.Status == entity.UserStatusSuspended { - session.Desc("suspended_at") - } else { - session.Desc("created_at") + session := ur.data.DB.Context(ctx) + switch user.Status { + case entity.UserStatusDeleted: + session.Desc("`user`.deleted_at") + case entity.UserStatusSuspended: + session.Desc("`user`.suspended_at") + default: + session.Desc("`user`.created_at") + } + + if len(usernameOrDisplayName) > 0 { + session.And(builder.Or( + builder.Like{"`user`.username", usernameOrDisplayName}, + builder.Like{"`user`.display_name", usernameOrDisplayName}, + )) } + if isStaff { + session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1") + } + total, err = pager.Help(page, pageSize, &users, user, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + tryToDecorateUserListFromUserCenter(ctx, ur.data, users) + return +} + +// DeletePermanentlyUsers delete permanently users +func (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) { + _, err = ur.data.DB.Context(ctx).Where("status = ?", entity.UserStatusDeleted).Delete(&entity.User{}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } + +// GetExpiredSuspendedUsers gets all suspended users whose suspension has expired +func (ur *userAdminRepo) GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) { + users = make([]*entity.User, 0) + now := time.Now() + + err = ur.data.DB.Context(ctx). + Where("status = ?", entity.UserStatusSuspended). + Where("suspended_until < ?", now). + Find(&users) + + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + return users, nil +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index 119bab410..a85cd79a1 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -1,45 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package user import ( "context" - "fmt" + "strings" "time" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/config" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" + "xorm.io/xorm" ) // userRepo user repository type userRepo struct { - data *data.Data - configRepo config.ConfigRepo + data *data.Data } // NewUserRepo new repository -func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserRepo { +func NewUserRepo(data *data.Data) usercommon.UserRepo { return &userRepo{ - data: data, - configRepo: configRepo, + data: data, } } // AddUser add user func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) { - _, err = ur.data.DB.Insert(user) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } + _, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) { + session = session.Context(ctx) + userInfo := &entity.User{} + exist, err := session.Where("username = ?", user.Username).Get(userInfo) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + return nil, errors.InternalServer(reason.UsernameDuplicate) + } + _, err = session.Insert(user) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil, nil + }) return } // IncreaseAnswerCount increase answer count func (ur *userRepo) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) { user := &entity.User{} - _, err = ur.data.DB.Where("id = ?", userID).Incr("answer_count", amount).Update(user) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("answer_count", amount).Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -49,7 +83,27 @@ func (ur *userRepo) IncreaseAnswerCount(ctx context.Context, userID string, amou // IncreaseQuestionCount increase question count func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) { user := &entity.User{} - _, err = ur.data.DB.Where("id = ?", userID).Incr("question_count", amount).Update(user) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("question_count", amount).Update(user) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) { + user := &entity.User{} + user.QuestionCount = int(count) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("question_count").Update(user) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) { + user := &entity.User{} + user.AnswerCount = count + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("answer_count").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -59,7 +113,7 @@ func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, am // UpdateLastLoginDate update last login date func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) { user := &entity.User{LastLoginDate: time.Now()} - _, err = ur.data.DB.Where("id = ?", userID).Cols("last_login_date").Update(user) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("last_login_date").Update(user) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -69,7 +123,7 @@ func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err // UpdateEmailStatus update email status func (ur *userRepo) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error { cond := &entity.User{MailStatus: emailStatus} - _, err := ur.data.DB.Where("id = ?", userID).Cols("mail_status").Update(cond) + _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("mail_status").Update(cond) if err != nil { return err } @@ -79,26 +133,32 @@ func (ur *userRepo) UpdateEmailStatus(ctx context.Context, userID string, emailS // UpdateNoticeStatus update notice status func (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error { cond := &entity.User{NoticeStatus: noticeStatus} - _, err := ur.data.DB.Where("id = ?", userID).Cols("notice_status").Update(cond) + _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("notice_status").Update(cond) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ur *userRepo) UpdatePass(ctx context.Context, Data *entity.User) error { - if Data.ID == "" { - return fmt.Errorf("input error") - } - _, err := ur.data.DB.Where("id = ?", Data.ID).Cols("pass").Update(Data) +func (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error { + _, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("pass").Update(&entity.User{Pass: pass}) if err != nil { - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err error) { - _, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{EMail: email}) + _, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Update(&entity.User{EMail: email}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (ur *userRepo) UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) { + session := ur.data.DB.Context(ctx).Where("id = ?", userID) + _, err = session.Cols("language", "color_scheme").Update(&entity.User{Language: language, ColorScheme: colorSchema}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -107,7 +167,7 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err // UpdateInfo update user info func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) { - _, err = ur.data.DB.Where("id = ?", userInfo.ID). + _, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID). Cols("username", "display_name", "avatar", "bio", "bio_html", "website", "location").Update(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -115,41 +175,223 @@ func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err return } +// UpdateUserProfile update user profile +func (ur *userRepo) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) { + _, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID). + Cols("username", "e_mail", "mail_status", "display_name").Update(userInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetByUserID get user info by user id func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} - exist, err = ur.data.DB.Where("id = ?", userID).Get(userInfo) + exist, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo) + if err != nil { + return nil, false, err } return } func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) { list := make([]*entity.User, 0) - err := ur.data.DB.In("id", ids).Find(&list) + err := ur.data.DB.Context(ctx).In("id", ids).Find(&list) if err != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + tryToDecorateUserListFromUserCenter(ctx, ur.data, list) return list, nil } // GetByUsername get user by username func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} - exist, err = ur.data.DB.Where("username = ?", username).Get(userInfo) + exist, err = ur.data.DB.Context(ctx).Where("username = ?", username).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo) + if err != nil { + return nil, false, err } return } +func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) { + list := make([]*entity.User, 0) + err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return list, err + } + tryToDecorateUserListFromUserCenter(ctx, ur.data, list) + return list, nil +} + // GetByEmail get user by email func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { userInfo = &entity.User{} - exist, err = ur.data.DB.Where("e_mail = ?", email).Get(userInfo) + exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email). + Where("status != ?", entity.UserStatusDeleted).Get(userInfo) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } + +func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { + session := ur.data.DB.Context(ctx) + session.Where("status = ? OR status = ?", entity.UserStatusAvailable, entity.UserStatusSuspended) + count, err = session.Count(&entity.User{}) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, nil +} + +func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int, + onlyStaff bool) (userList []*entity.User, err error) { + userList = make([]*entity.User, 0) + session := ur.data.DB.Context(ctx) + if onlyStaff { + session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1") + } + session.Where("status = ?", entity.UserStatusAvailable) + session.Where("username LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%") + session.OrderBy("username ASC, `user`.id DESC") + session.Limit(limit) + err = session.Find(&userList) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + tryToDecorateUserListFromUserCenter(ctx, ur.data, userList) + return +} + +func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) { + if original == nil { + return nil + } + uc, ok := plugin.GetUserCenter() + if !ok { + return nil + } + + userInfo := &entity.UserExternalLogin{} + session := data.DB.Context(ctx).Where("user_id = ?", original.ID) + session.Where("provider = ?", uc.Info().SlugName) + exist, err := session.Get(userInfo) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return nil + } + + userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID) + if err != nil { + log.Error(err) + return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack() + } + + decorateByUserCenterUser(original, userCenterBasicUserInfo) + return nil +} + +func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) { + uc, ok := plugin.GetUserCenter() + if !ok { + return + } + + ids := make([]string, 0) + originalUserIDMapping := make(map[string]*entity.User, 0) + for _, user := range original { + originalUserIDMapping[user.ID] = user + ids = append(ids, user.ID) + } + + userExternalLoginList := make([]*entity.UserExternalLogin, 0) + session := data.DB.Context(ctx).Where("provider = ?", uc.Info().SlugName) + session.In("user_id", ids) + err := session.Find(&userExternalLoginList) + if err != nil { + log.Error(err) + return + } + + userExternalIDs := make([]string, 0) + originalExternalIDMapping := make(map[string]*entity.User, 0) + for _, u := range userExternalLoginList { + originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID] + userExternalIDs = append(userExternalIDs, u.ExternalID) + } + if len(userExternalIDs) == 0 { + return + } + + ucUsers, err := uc.UserList(userExternalIDs) + if err != nil { + log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs) + return + } + + for _, ucUser := range ucUsers { + decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser) + } +} + +func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) { + if original == nil || ucUser == nil { + return + } + // In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies. + if original.Username != ucUser.Username { + log.Warnf("user %s username is inconsistent with user center", original.ID) + } + if len(ucUser.DisplayName) > 0 { + original.DisplayName = ucUser.DisplayName + } + if len(ucUser.Email) > 0 { + original.EMail = ucUser.Email + } + if len(ucUser.Avatar) > 0 { + original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString() + } + if len(ucUser.Mobile) > 0 { + original.Mobile = ucUser.Mobile + } + if len(ucUser.Bio) > 0 { + original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML + } + + // If plugin enable rank agent, use rank from user center. + if plugin.RankAgentEnabled() { + original.Rank = ucUser.Rank + } + if ucUser.Status != plugin.UserStatusAvailable { + original.Status = int(ucUser.Status) + } +} + +func (ur *userRepo) IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) { + user := &entity.User{} + count, err := ur.data.DB.Context(ctx). + Table("user"). + Where(builder.Like{"avatar", "%" + filePath + "%"}). + Count(&user) + + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + return count > 0, nil +} diff --git a/internal/repo/user_external_login/user_external_login_repo.go b/internal/repo/user_external_login/user_external_login_repo.go new file mode 100644 index 000000000..c797e461d --- /dev/null +++ b/internal/repo/user_external_login/user_external_login_repo.go @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_external_login + +import ( + "context" + "encoding/json" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/segmentfault/pacman/errors" +) + +type userExternalLoginRepo struct { + data *data.Data +} + +// NewUserExternalLoginRepo new repository +func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo { + return &userExternalLoginRepo{ + data: data, + } +} + +// AddUserExternalLogin add external login information +func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) { + _, err = ur.data.DB.Context(ctx).Insert(user) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// UpdateInfo update user info +func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) { + _, err = ur.data.DB.Context(ctx).ID(userInfo.ID).Update(userInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetByExternalID get by external ID +func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) ( + userInfo *entity.UserExternalLogin, exist bool, err error) { + userInfo = &entity.UserExternalLogin{} + exist, err = ur.data.DB.Context(ctx).Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetByUserID get by user ID +func (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, userID string) ( + userInfo *entity.UserExternalLogin, exist bool, err error) { + userInfo = &entity.UserExternalLogin{} + exist, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Where("provider = ?", provider).Get(userInfo) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// GetUserExternalLoginList get by external ID +func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) ( + resp []*entity.UserExternalLogin, err error) { + resp = make([]*entity.UserExternalLogin, 0) + err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&resp) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// DeleteUserExternalLogin delete external user login info +func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) { + cond := &entity.UserExternalLogin{} + _, err = ur.data.DB.Context(ctx).Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// DeleteUserExternalLoginByUserID delete external user login info by user ID +func (ur *userExternalLoginRepo) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) { + cond := &entity.UserExternalLogin{} + _, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +// SetCacheUserExternalLoginInfo cache user info for external login +func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo( + ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) { + cacheData, _ := json.Marshal(info) + return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key, + string(cacheData), constant.ConnectorUserExternalInfoCacheTime) +} + +// GetCacheUserExternalLoginInfo cache user info for external login +func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo( + ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) { + res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key) + if err != nil { + return info, err + } + if !exist { + return nil, nil + } + info = &schema.ExternalLoginUserInfoCache{} + _ = json.Unmarshal([]byte(res), &info) + return info, nil +} diff --git a/internal/repo/user_notification_config/user_notification_config_repo.go b/internal/repo/user_notification_config/user_notification_config_repo.go new file mode 100644 index 000000000..8ea2b065b --- /dev/null +++ b/internal/repo/user_notification_config/user_notification_config_repo.go @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_notification_config + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/user_notification_config" + "github.com/segmentfault/pacman/errors" +) + +// userNotificationConfigRepo notification repository +type userNotificationConfigRepo struct { + data *data.Data +} + +// NewUserNotificationConfigRepo new repository +func NewUserNotificationConfigRepo(data *data.Data) user_notification_config.UserNotificationConfigRepo { + return &userNotificationConfigRepo{ + data: data, + } +} + +// Add add notification config +func (ur *userNotificationConfigRepo) Add(ctx context.Context, userIDs []string, source, channels string) (err error) { + var configs []*entity.UserNotificationConfig + for _, userID := range userIDs { + configs = append(configs, &entity.UserNotificationConfig{ + UserID: userID, + Source: source, + Channels: channels, + Enabled: true, + }) + } + _, err = ur.data.DB.Context(ctx).Insert(configs) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +// Save save notification config, if existed, update, if not exist, insert +func (ur *userNotificationConfigRepo) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) { + old := &entity.UserNotificationConfig{UserID: uc.UserID, Source: uc.Source} + exist, err := ur.data.DB.Context(ctx).Get(old) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if exist { + old.Channels = uc.Channels + old.Enabled = uc.Enabled + _, err = ur.data.DB.Context(ctx).ID(old.ID).UseBool("enabled").Cols("channels", "enabled").Update(old) + } else { + _, err = ur.data.DB.Context(ctx).Insert(uc) + } + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + +// GetByUserID get notification config by user id +func (ur *userNotificationConfigRepo) GetByUserID(ctx context.Context, userID string) ( + []*entity.UserNotificationConfig, error) { + var configs []*entity.UserNotificationConfig + err := ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&configs) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return configs, nil +} + +// GetBySource get notification config by source +func (ur *userNotificationConfigRepo) GetBySource(ctx context.Context, source constant.NotificationSource) ( + []*entity.UserNotificationConfig, error) { + var configs []*entity.UserNotificationConfig + err := ur.data.DB.Context(ctx).UseBool("enabled"). + Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return configs, nil +} + +// GetByUserIDAndSource get notification config by user id and source +func (ur *userNotificationConfigRepo) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) ( + conf *entity.UserNotificationConfig, exist bool, err error) { + config := &entity.UserNotificationConfig{UserID: userID, Source: string(source)} + exist, err = ur.data.DB.Context(ctx).Get(config) + if err != nil { + return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return config, exist, nil +} + +// GetByUsersAndSource get notification config by user ids and source +func (ur *userNotificationConfigRepo) GetByUsersAndSource( + ctx context.Context, userIDs []string, source constant.NotificationSource) ( + []*entity.UserNotificationConfig, error) { + var configs []*entity.UserNotificationConfig + err := ur.data.DB.Context(ctx).UseBool("enabled").In("user_id", userIDs). + Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true}) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return configs, nil +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index e227a10fe..1191492b9 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -1,32 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router import ( - "github.com/answerdev/answer/internal/controller" - "github.com/answerdev/answer/internal/controller_backyard" + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/controller" + "github.com/apache/answer/internal/controller_admin" "github.com/gin-gonic/gin" ) type AnswerAPIRouter struct { - langController *controller.LangController - userController *controller.UserController - commentController *controller.CommentController - reportController *controller.ReportController - voteController *controller.VoteController - tagController *controller.TagController - followController *controller.FollowController - collectionController *controller.CollectionController - questionController *controller.QuestionController - answerController *controller.AnswerController - searchController *controller.SearchController - revisionController *controller.RevisionController - rankController *controller.RankController - backyardReportController *controller_backyard.ReportController - backyardUserController *controller_backyard.UserBackyardController - reasonController *controller.ReasonController - themeController *controller_backyard.ThemeController - siteInfoController *controller_backyard.SiteInfoController - siteinfoController *controller.SiteinfoController - notificationController *controller.NotificationController + langController *controller.LangController + userController *controller.UserController + commentController *controller.CommentController + reportController *controller.ReportController + voteController *controller.VoteController + tagController *controller.TagController + followController *controller.FollowController + collectionController *controller.CollectionController + questionController *controller.QuestionController + answerController *controller.AnswerController + searchController *controller.SearchController + revisionController *controller.RevisionController + rankController *controller.RankController + adminUserController *controller_admin.UserAdminController + reasonController *controller.ReasonController + themeController *controller_admin.ThemeController + adminSiteInfoController *controller_admin.SiteInfoController + siteInfoController *controller.SiteInfoController + notificationController *controller.NotificationController + dashboardController *controller.DashboardController + uploadController *controller.UploadController + activityController *controller.ActivityController + roleController *controller_admin.RoleController + pluginController *controller_admin.PluginController + permissionController *controller.PermissionController + userPluginController *controller.UserPluginController + reviewController *controller.ReviewController + metaController *controller.MetaController + badgeController *controller.BadgeController + adminBadgeController *controller_admin.BadgeController } func NewAnswerAPIRouter( @@ -43,96 +73,150 @@ func NewAnswerAPIRouter( searchController *controller.SearchController, revisionController *controller.RevisionController, rankController *controller.RankController, - backyardReportController *controller_backyard.ReportController, - backyardUserController *controller_backyard.UserBackyardController, + adminUserController *controller_admin.UserAdminController, reasonController *controller.ReasonController, - themeController *controller_backyard.ThemeController, - siteInfoController *controller_backyard.SiteInfoController, - siteinfoController *controller.SiteinfoController, + themeController *controller_admin.ThemeController, + adminSiteInfoController *controller_admin.SiteInfoController, + siteInfoController *controller.SiteInfoController, notificationController *controller.NotificationController, - + dashboardController *controller.DashboardController, + uploadController *controller.UploadController, + activityController *controller.ActivityController, + roleController *controller_admin.RoleController, + pluginController *controller_admin.PluginController, + permissionController *controller.PermissionController, + userPluginController *controller.UserPluginController, + reviewController *controller.ReviewController, + metaController *controller.MetaController, + badgeController *controller.BadgeController, + adminBadgeController *controller_admin.BadgeController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ - langController: langController, - userController: userController, - commentController: commentController, - reportController: reportController, - voteController: voteController, - tagController: tagController, - followController: followController, - collectionController: collectionController, - questionController: questionController, - answerController: answerController, - searchController: searchController, - revisionController: revisionController, - rankController: rankController, - backyardReportController: backyardReportController, - backyardUserController: backyardUserController, - reasonController: reasonController, - themeController: themeController, - siteInfoController: siteInfoController, - notificationController: notificationController, - siteinfoController: siteinfoController, + langController: langController, + userController: userController, + commentController: commentController, + reportController: reportController, + voteController: voteController, + tagController: tagController, + followController: followController, + collectionController: collectionController, + questionController: questionController, + answerController: answerController, + searchController: searchController, + revisionController: revisionController, + rankController: rankController, + adminUserController: adminUserController, + reasonController: reasonController, + themeController: themeController, + adminSiteInfoController: adminSiteInfoController, + notificationController: notificationController, + siteInfoController: siteInfoController, + dashboardController: dashboardController, + uploadController: uploadController, + activityController: activityController, + roleController: roleController, + pluginController: pluginController, + permissionController: permissionController, + userPluginController: userPluginController, + reviewController: reviewController, + metaController: metaController, + badgeController: badgeController, + adminBadgeController: adminBadgeController, } } -func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { +func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *middleware.AuthUserMiddleware, r *gin.RouterGroup) { // i18n r.GET("/language/config", a.langController.GetLangMapping) - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetUserLangOptions) - // comment - r.GET("/comment/page", a.commentController.GetCommentWithPage) - r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) - r.GET("/comment", a.commentController.GetComment) + // siteinfo + r.GET("/siteinfo", a.siteInfoController.GetSiteInfo) + r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegalInfo) + + // user + r.GET("/user/info", a.userController.GetUserInfoByUserID) + r.GET("/user/action/record", authUserMiddleware.Auth(), a.userController.ActionRecord) + routerGroup := r.Group("", middleware.BanAPIForUserCenter) + routerGroup.POST("/user/login/email", a.userController.UserEmailLogin) + routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail) + routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail) + routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify) + routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord) + routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord) + routerGroup.PUT("/user/notification/unsubscribe", a.userController.UserUnsubscribeNotification) + + // plugins + r.GET("/plugin/status", a.pluginController.GetAllPluginStatus) +} +func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // user - r.GET("/user/status", a.userController.GetUserStatus) - r.GET("/user/action/record", a.userController.ActionRecord) - r.POST("/user/login/email", a.userController.UserEmailLogin) - r.POST("/user/register/email", a.userController.UserRegisterByEmail) - r.POST("/user/email/verification", a.userController.UserVerifyEmail) - r.POST("/user/password/reset", a.userController.RetrievePassWord) - r.POST("/user/password/replacement", a.userController.UseRePassWord) r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername) - r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend) - r.GET("/user/logout", a.userController.UserLogout) - r.PUT("/user/email", a.userController.UserChangeEmailVerify) + r.GET("/user/ranking", a.userController.UserRanking) + r.GET("/user/staff", a.userController.UserStaff) - //answer - r.GET("/answer/info", a.answerController.Get) + // answer + r.GET("/answer/info", a.answerController.GetAnswerInfo) r.GET("/answer/page", a.answerController.AnswerList) - r.GET("/personal/answer/page", a.questionController.UserAnswerList) + r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage) - //question + // question r.GET("/question/info", a.questionController.GetQuestion) - r.POST("/question/search", a.questionController.SearchList) - r.GET("/question/page", a.questionController.Index) + r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo) + r.GET("/question/page", a.questionController.QuestionPage) + r.GET("/question/recommend/page", a.questionController.QuestionRecommendPage) r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/personal/qa/top", a.questionController.UserTop) - r.GET("/personal/question/page", a.questionController.UserList) + r.GET("/personal/question/page", a.questionController.PersonalQuestionPage) + r.GET("/question/link", a.questionController.GetQuestionLink) - //revision + // comment + r.GET("/comment/page", a.commentController.GetCommentWithPage) + r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) + r.GET("/comment", a.commentController.GetComment) + + // revision r.GET("/revisions", a.revisionController.GetRevisionList) // tag r.GET("/tags/page", a.tagController.GetTagWithPage) r.GET("/tags/following", a.tagController.GetFollowingTags) r.GET("/tag", a.tagController.GetTagInfo) + r.GET("/tags", a.tagController.GetTagsBySlugName) r.GET("/tag/synonyms", a.tagController.GetTagSynonyms) - r.GET("/question/index", a.questionController.Index) - //search + // search r.GET("/search", a.searchController.Search) + r.GET("/search/desc", a.searchController.SearchDesc) - //rank + // rank r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) - //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + // reaction + r.GET("/meta/reaction", a.metaController.GetReaction) + + // badges + r.GET("/badge", a.badgeController.GetBadgeInfo) + r.GET("/badge/awards/page", a.badgeController.GetBadgeAwardList) + r.GET("/badge/user/awards/recent", a.badgeController.GetRecentBadgeAwardListByUsername) + r.GET("/badge/user/awards", a.badgeController.GetAllBadgeAwardListByUsername) + r.GET("/badges", a.badgeController.GetBadgeList) +} + +func (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r *gin.RouterGroup) { + r.GET("/user/logout", a.userController.UserLogout) + r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode) + r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { + // revisions + r.GET("/revisions/unreviewed", a.revisionController.GetUnreviewedRevisionList) + r.PUT("/revisions/audit", a.revisionController.RevisionAudit) + r.GET("/revisions/edit/check", a.revisionController.CheckCanUpdateRevision) + r.GET("/reviewing/type", a.revisionController.GetReviewingType) + // comment r.POST("/comment", a.commentController.AddComment) r.DELETE("/comment", a.commentController.RemoveComment) @@ -140,6 +224,12 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // report r.POST("/report", a.reportController.AddReport) + r.GET("/report/unreviewed/post", a.reportController.GetUnreviewedReportPostPage) + r.PUT("/report/review", a.reportController.ReviewReport) + + // review + r.GET("/review/pending/post/page", a.reviewController.GetUnreviewedPostPage) + r.PUT("/review/pending/post", a.reviewController.UpdateReview) // vote r.POST("/vote/up", a.voteController.VoteUp) @@ -151,35 +241,43 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // tag r.GET("/question/tags", a.tagController.SearchTagLike) + r.POST("/tag", a.tagController.AddTag) r.PUT("/tag", a.tagController.UpdateTag) + r.POST("/tag/recover", a.tagController.RecoverTag) r.DELETE("/tag", a.tagController.RemoveTag) r.PUT("/tag/synonym", a.tagController.UpdateTagSynonym) + r.POST("/tag/merge", a.tagController.MergeTag) // collection r.POST("/collection/switch", a.collectionController.CollectionSwitch) - r.GET("/personal/collection/page", a.questionController.UserCollectionList) + r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage) // question r.POST("/question", a.questionController.AddQuestion) + r.POST("/question/answer", a.questionController.AddQuestionByAnswer) r.PUT("/question", a.questionController.UpdateQuestion) + r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser) r.DELETE("/question", a.questionController.RemoveQuestion) r.PUT("/question/status", a.questionController.CloseQuestion) - r.GET("/question/similar", a.questionController.SearchByTitleLike) + r.PUT("/question/operation", a.questionController.OperationQuestion) + r.PUT("/question/reopen", a.questionController.ReopenQuestion) + r.GET("/question/similar", a.questionController.GetSimilarQuestions) + r.POST("/question/recover", a.questionController.QuestionRecover) // answer - r.POST("/answer", a.answerController.Add) - r.PUT("/answer", a.answerController.Update) - r.POST("/answer/acceptance", a.answerController.Adopted) + r.POST("/answer", a.answerController.AddAnswer) + r.PUT("/answer", a.answerController.UpdateAnswer) + r.POST("/answer/acceptance", a.answerController.AcceptAnswer) r.DELETE("/answer", a.answerController.RemoveAnswer) + r.POST("/answer/recover", a.answerController.RecoverAnswer) // user - r.GET("/user/info", a.userController.GetUserInfoByUserID) - r.PUT("/user/password", a.userController.UserModifyPassWord) + r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) - r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) - r.POST("/user/post/file", a.userController.UploadUserPostFile) - r.POST("/user/notice/set", a.userController.UserNoticeSet) - r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode) + r.PUT("/user/interface", a.userController.UserUpdateInterface) + r.GET("/user/notification/config", a.userController.GetUserNotificationConfig) + r.PUT("/user/notification/config", a.userController.UpdateUserNotificationConfig) + r.GET("/user/info/search", a.userController.SearchUserListByName) // vote r.GET("/personal/vote/page", a.voteController.UserVotes) @@ -187,42 +285,100 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // reason r.GET("/reasons", a.reasonController.Reasons) + // permission + r.GET("/permission", a.permissionController.GetPermission) + // notification r.GET("/notification/status", a.notificationController.GetRedDot) r.PUT("/notification/status", a.notificationController.ClearRedDot) r.GET("/notification/page", a.notificationController.GetList) r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead) r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead) -} -func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { - r.GET("/question/page", a.questionController.CmsSearchList) - r.PUT("/question/status", a.questionController.AdminSetQuestionStatus) - r.GET("/answer/page", a.questionController.CmsSearchAnswerList) - r.PUT("/answer/status", a.answerController.AdminSetAnswerStatus) + // upload file + r.POST("/file", a.uploadController.UploadFile) + r.POST("/post/render", a.uploadController.PostRender) - // report - r.GET("/reports/page", a.backyardReportController.ListReportPage) - r.PUT("/report", a.backyardReportController.Handle) + // activity + r.GET("/activity/timeline", a.activityController.GetObjectTimeline) + r.GET("/activity/timeline/detail", a.activityController.GetObjectTimelineDetail) + + // plugin + r.GET("/user/plugin/configs", a.userPluginController.GetUserPluginList) + r.GET("/user/plugin/config", a.userPluginController.GetUserPluginConfig) + r.PUT("/user/plugin/config", a.userPluginController.UpdatePluginUserConfig) + + // meta + r.PUT("/meta/reaction", a.metaController.AddOrUpdateReaction) +} + +func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { + r.GET("/question/page", a.questionController.AdminQuestionPage) + r.PUT("/question/status", a.questionController.AdminUpdateQuestionStatus) + r.GET("/answer/page", a.questionController.AdminAnswerPage) + r.PUT("/answer/status", a.answerController.AdminUpdateAnswerStatus) // user - r.GET("/users/page", a.backyardUserController.GetUserPage) - r.PUT("/user/status", a.backyardUserController.UpdateUserStatus) + r.GET("/users/page", a.adminUserController.GetUserPage) + r.PUT("/user/status", a.adminUserController.UpdateUserStatus) + r.PUT("/user/role", a.adminUserController.UpdateUserRole) + r.GET("/user/activation", a.adminUserController.GetUserActivation) + r.POST("/user/activation", a.adminUserController.SendUserActivation) + r.POST("/user", a.adminUserController.AddUser) + r.POST("/users", a.adminUserController.AddUsers) + r.PUT("/user/password", a.adminUserController.UpdateUserPassword) + r.PUT("/user/profile", a.adminUserController.EditUserProfile) + + r.DELETE("/delete/permanently", a.adminUserController.DeletePermanently) // reason r.GET("/reasons", a.reasonController.Reasons) // language - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetAdminLangOptions) // theme r.GET("/theme/options", a.themeController.GetThemeOptions) // siteinfo - r.GET("/siteinfo/general", a.siteInfoController.GetGeneral) - r.GET("/siteinfo/interface", a.siteInfoController.GetInterface) - r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral) - r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface) - r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig) - r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig) + r.GET("/siteinfo/general", a.adminSiteInfoController.GetGeneral) + r.PUT("/siteinfo/general", a.adminSiteInfoController.UpdateGeneral) + r.GET("/siteinfo/interface", a.adminSiteInfoController.GetInterface) + r.PUT("/siteinfo/interface", a.adminSiteInfoController.UpdateInterface) + r.GET("/siteinfo/branding", a.adminSiteInfoController.GetSiteBranding) + r.PUT("/siteinfo/branding", a.adminSiteInfoController.UpdateBranding) + r.GET("/siteinfo/write", a.adminSiteInfoController.GetSiteWrite) + r.PUT("/siteinfo/write", a.adminSiteInfoController.UpdateSiteWrite) + r.GET("/siteinfo/legal", a.adminSiteInfoController.GetSiteLegal) + r.PUT("/siteinfo/legal", a.adminSiteInfoController.UpdateSiteLegal) + r.GET("/siteinfo/seo", a.adminSiteInfoController.GetSeo) + r.PUT("/siteinfo/seo", a.adminSiteInfoController.UpdateSeo) + r.GET("/siteinfo/login", a.adminSiteInfoController.GetSiteLogin) + r.PUT("/siteinfo/login", a.adminSiteInfoController.UpdateSiteLogin) + r.GET("/siteinfo/custom-css-html", a.adminSiteInfoController.GetSiteCustomCssHTML) + r.PUT("/siteinfo/custom-css-html", a.adminSiteInfoController.UpdateSiteCustomCssHTML) + r.GET("/siteinfo/theme", a.adminSiteInfoController.GetSiteTheme) + r.PUT("/siteinfo/theme", a.adminSiteInfoController.SaveSiteTheme) + r.GET("/siteinfo/users", a.adminSiteInfoController.GetSiteUsers) + r.PUT("/siteinfo/users", a.adminSiteInfoController.UpdateSiteUsers) + r.GET("/setting/smtp", a.adminSiteInfoController.GetSMTPConfig) + r.PUT("/setting/smtp", a.adminSiteInfoController.UpdateSMTPConfig) + r.GET("/setting/privileges", a.adminSiteInfoController.GetPrivilegesConfig) + r.PUT("/setting/privileges", a.adminSiteInfoController.UpdatePrivilegesConfig) + + // dashboard + r.GET("/dashboard", a.dashboardController.DashboardInfo) + + // roles + r.GET("/roles", a.roleController.GetRoleList) + + // plugin + r.GET("/plugins", a.pluginController.GetPluginList) + r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus) + r.GET("/plugin/config", a.pluginController.GetPluginConfig) + r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig) + + // badge + r.GET("/badges", a.adminBadgeController.GetBadgeList) + r.PUT("/badge/status", a.adminBadgeController.UpdateBadgeStatus) } diff --git a/internal/router/config.go b/internal/router/config.go index 742c9c8cf..1937ffbc3 100644 --- a/internal/router/config.go +++ b/internal/router/config.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router // SwaggerConfig struct describes configure for the Swagger API endpoint type SwaggerConfig struct { - Show bool `json:"show"` - Protocol string `json:"protocol"` - Host string `json:"host"` - Address string `json:"address"` + Show bool `json:"show" mapstructure:"show" yaml:"show"` + Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"` + Host string `json:"host" mapstructure:"host" yaml:"host"` + Address string `json:"address" mapstructure:"address" yaml:"address"` } diff --git a/internal/router/plugin_api_router.go b/internal/router/plugin_api_router.go new file mode 100644 index 000000000..ce5062ba5 --- /dev/null +++ b/internal/router/plugin_api_router.go @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package router + +import ( + "github.com/apache/answer/internal/controller" + "github.com/gin-gonic/gin" +) + +type PluginAPIRouter struct { + connectorController *controller.ConnectorController + userCenterController *controller.UserCenterController + captchaController *controller.CaptchaController + embedController *controller.EmbedController + renderController *controller.RenderController +} + +func NewPluginAPIRouter( + connectorController *controller.ConnectorController, + userCenterController *controller.UserCenterController, + captchaController *controller.CaptchaController, + embedController *controller.EmbedController, + renderController *controller.RenderController, +) *PluginAPIRouter { + return &PluginAPIRouter{ + connectorController: connectorController, + userCenterController: userCenterController, + captchaController: captchaController, + embedController: embedController, + renderController: renderController, + } +} + +func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) { + // connector plugin + connectorController := pr.connectorController + r.GET(controller.ConnectorLoginRouterPrefix+":name", connectorController.ConnectorLoginDispatcher) + r.GET(controller.ConnectorRedirectRouterPrefix+":name", connectorController.ConnectorRedirectDispatcher) + r.GET("/connector/info", connectorController.ConnectorsInfo) + r.POST("/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail) + + // user center plugin + r.GET("/user-center/agent", pr.userCenterController.UserCenterAgent) + r.GET("/user-center/personal/branding", pr.userCenterController.UserCenterPersonalBranding) + r.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect) + r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect) + r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback) + r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback) + + // captcha plugin + r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig) + r.GET("/embed/config", pr.embedController.GetEmbedConfig) + r.GET("/render/config", pr.renderController.GetRenderConfig) +} + +func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) { + connectorController := pr.connectorController + r.GET("/connector/user/info", connectorController.ConnectorsUserInfo) + r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding) + + r.GET("/user-center/user/settings", pr.userCenterController.UserCenterUserSettings) +} + +func (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) { + r.GET("/user-center/agent", pr.userCenterController.UserCenterAdminFunctionAgent) +} diff --git a/internal/router/provider.go b/internal/router/provider.go index 08df9ce58..2952de0a2 100644 --- a/internal/router/provider.go +++ b/internal/router/provider.go @@ -1,6 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router import "github.com/google/wire" // ProviderSetRouter is providers. -var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter) +var ProviderSetRouter = wire.NewSet( + NewAnswerAPIRouter, + NewSwaggerRouter, + NewStaticRouter, + NewUIRouter, + NewTemplateRouter, + NewPluginAPIRouter, +) diff --git a/internal/router/static_router.go b/internal/router/static_router.go index 77c32c0ba..a6c80fc04 100644 --- a/internal/router/static_router.go +++ b/internal/router/static_router.go @@ -1,7 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router import ( - "github.com/answerdev/answer/internal/service/service_config" + "net/http" + "path/filepath" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/pkg/dir" "github.com/gin-gonic/gin" ) @@ -19,5 +44,24 @@ func NewStaticRouter(serviceConfig *service_config.ServiceConfig) *StaticRouter // RegisterStaticRouter register static api router func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) { - r.Static("/uploads", a.serviceConfig.UploadPath) + r.Static("/uploads/"+constant.AvatarSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarSubPath)) + r.Static("/uploads/"+constant.AvatarThumbSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarThumbSubPath)) + r.Static("/uploads/"+constant.PostSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.PostSubPath)) + r.Static("/uploads/"+constant.BrandingSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.BrandingSubPath)) + r.GET("/uploads/"+constant.FilesPostSubPath+"/*filepath", func(c *gin.Context) { + // The filepath such as hash/123.pdf + filePath := c.Param("filepath") + // The original filename is 123.pdf + originalFilename := filepath.Base(filePath) + // The real filename is hash.pdf + realFilename := strings.TrimSuffix(filePath, "/"+originalFilename) + filepath.Ext(originalFilename) + // The file local path is /uploads/files/post/hash.pdf + fileLocalPath := filepath.Join(a.serviceConfig.UploadPath, constant.FilesPostSubPath, realFilename) + // If the file is not exist, return 404 + if !dir.CheckFileExist(fileLocalPath) { + c.Redirect(http.StatusFound, "/404") + return + } + c.FileAttachment(fileLocalPath, originalFilename) + }) } diff --git a/internal/router/swagger_router.go b/internal/router/swagger_router.go index b0fd7952e..06c13588f 100644 --- a/internal/router/swagger_router.go +++ b/internal/router/swagger_router.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router import ( "fmt" - "github.com/answerdev/answer/docs" + "github.com/apache/answer/docs" "github.com/gin-gonic/gin" swaggerfiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" @@ -32,9 +51,5 @@ func (a *SwaggerRouter) Register(r *gin.RouterGroup) { // InitSwaggerDocs init swagger docs func (a *SwaggerRouter) InitSwaggerDocs() { - docs.SwaggerInfo.Title = "answer" - docs.SwaggerInfo.Description = "answer api" - docs.SwaggerInfo.Version = "v0.0.1" docs.SwaggerInfo.Host = fmt.Sprintf("%s%s", a.config.Host, a.config.Address) - docs.SwaggerInfo.BasePath = "/" } diff --git a/internal/router/template_router.go b/internal/router/template_router.go new file mode 100644 index 000000000..7c42f4ea3 --- /dev/null +++ b/internal/router/template_router.go @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package router + +import ( + "github.com/apache/answer/internal/base/middleware" + "github.com/apache/answer/internal/controller" + templaterender "github.com/apache/answer/internal/controller/template_render" + "github.com/apache/answer/internal/controller_admin" + "github.com/gin-gonic/gin" +) + +type TemplateRouter struct { + templateController *controller.TemplateController + templateRenderController *templaterender.TemplateRenderController + siteInfoController *controller_admin.SiteInfoController + authUserMiddleware *middleware.AuthUserMiddleware +} + +func NewTemplateRouter( + templateController *controller.TemplateController, + templateRenderController *templaterender.TemplateRenderController, + siteInfoController *controller_admin.SiteInfoController, + authUserMiddleware *middleware.AuthUserMiddleware, + +) *TemplateRouter { + return &TemplateRouter{ + templateController: templateController, + templateRenderController: templateRenderController, + siteInfoController: siteInfoController, + authUserMiddleware: authUserMiddleware, + } +} + +// RegisterTemplateRouter template router +func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup, baseURLPath string) { + seoNoAuth := r.Group(baseURLPath) + seoNoAuth.GET("/sitemap.xml", a.templateController.Sitemap) + seoNoAuth.GET("/sitemap/:page", a.templateController.SitemapPage) + + seoNoAuth.GET("/robots.txt", a.siteInfoController.GetRobots) + seoNoAuth.GET("/custom.css", a.siteInfoController.GetCss) + + seoNoAuth.GET("/404", a.templateController.Page404) + + seoNoAuth.GET("/opensearch.xml", a.templateController.OpenSearch) + + seo := r.Group(baseURLPath) + seo.Use(a.authUserMiddleware.CheckPrivateMode()) + seo.GET("/", a.templateController.Index) + seo.GET("/questions", a.templateController.QuestionList) + seo.GET("/questions/:id", a.templateController.QuestionInfo) + seo.GET("/questions/:id/:title", a.templateController.QuestionInfo) + seo.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo) + seo.GET("/tags", a.templateController.TagList) + seo.GET("/tags/:tag", a.templateController.TagInfo) + seo.GET("/users/:username", a.templateController.UserInfo) +} diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c6..0b5ee3f96 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -1,13 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package router import ( "embed" "fmt" + "github.com/apache/answer/plugin" "io/fs" "net/http" "os" + "strings" - "github.com/answerdev/answer/ui" + "github.com/apache/answer/internal/controller" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" ) @@ -18,11 +42,19 @@ const UIStaticPath = "build/static" // UIRouter is an interface that provides ui static file routers type UIRouter struct { + siteInfoController *controller.SiteInfoController + siteInfoService siteinfo_common.SiteInfoCommonService } // NewUIRouter creates a new UIRouter instance with the embed resources -func NewUIRouter() *UIRouter { - return &UIRouter{} +func NewUIRouter( + siteInfoController *controller.SiteInfoController, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *UIRouter { + return &UIRouter{ + siteInfoController: siteInfoController, + siteInfoService: siteInfoService, + } } // _resource is an interface that provides static file, it's a private interface @@ -38,7 +70,7 @@ func (r *_resource) Open(name string) (fs.File, error) { } // Register a new static resource which generated by ui directory -func (a *UIRouter) Register(r *gin.Engine) { +func (a *UIRouter) Register(r *gin.Engine, baseURLPath string) { staticPath := os.Getenv("ANSWER_STATIC_PATH") // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource @@ -51,7 +83,7 @@ func (a *UIRouter) Register(r *gin.Engine) { log.Debugf("registering static path %s", staticPath) r.LoadHTMLGlob(staticPath + "/*.html") - r.Static("/static", staticPath+"/static") + r.Static(baseURLPath+"/static", staticPath+"/static") r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) @@ -62,32 +94,75 @@ func (a *UIRouter) Register(r *gin.Engine) { } // handle the static file by default ui static files - r.StaticFS("/static", http.FS(&_resource{ + r.StaticFS(baseURLPath+"/static", http.FS(&_resource{ fs: ui.Build, })) // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { - name := c.Request.URL.Path + urlPath := c.Request.URL.Path + if len(baseURLPath) > 0 { + urlPath = strings.TrimPrefix(urlPath, baseURLPath) + } filePath := "" - var file []byte - var err error - switch name { + switch urlPath { case "/favicon.ico": - c.Header("content-type", "image/vnd.microsoft.icon") - filePath = UIRootFilePath + name + branding, err := a.siteInfoService.GetSiteBranding(c) + if err != nil { + log.Error(err) + } + if branding.Favicon != "" { + c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon)) + return + } else if branding.SquareIcon != "" { + c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon)) + return + } else { + c.Header("content-type", "image/vnd.microsoft.icon") + filePath = UIRootFilePath + urlPath + } case "/manifest.json": - filePath = UIRootFilePath + name + a.siteInfoController.GetManifestJson(c) + return + case "/install": + // if answer is running by run command user can not access install page. + c.Redirect(http.StatusFound, "/") + return default: filePath = UIIndexFilePath c.Header("content-type", "text/html;charset=utf-8") + c.Header("X-Frame-Options", "DENY") } - file, err = ui.Build.ReadFile(filePath) + file, err := ui.Build.ReadFile(filePath) if err != nil { log.Error(err) c.Status(http.StatusNotFound) return } + + cdnPrefix := "" + _ = plugin.CallCDN(func(fn plugin.CDN) error { + cdnPrefix = fn.GetStaticPrefix() + return nil + }) + if cdnPrefix != "" { + if cdnPrefix[len(cdnPrefix)-1:] == "/" { + cdnPrefix = strings.TrimSuffix(cdnPrefix, "/") + } + c.String(http.StatusOK, strings.ReplaceAll(string(file), "/static", cdnPrefix+"/static")) + return + } + + // This part is to solve the problem of returning 404 when the access path does not exist. + // However, there is no way to check whether the current route exists in the frontend. + // We can only hand over the route to the frontend for processing. + // And the plugin, frontend routes can now be dynamically registered, + // so there's no good way to get all frontend routes + //if filePath == UIIndexFilePath { + // c.String(http.StatusNotFound, string(file)) + // return + //} + c.String(http.StatusOK, string(file)) }) } diff --git a/internal/router/ui_test.go b/internal/router/ui_test.go deleted file mode 100644 index 1ec160e93..000000000 --- a/internal/router/ui_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package router - -import ( - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -func TestUIRouter_Register(t *testing.T) { - r := gin.Default() - - NewUIRouter().Register(r) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) -} - -func TestUIRouter_Static(t *testing.T) { - r := gin.Default() - - NewUIRouter().Register(r) - - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/static/version.txt", nil) - - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "OK", w.Body.String()) -} diff --git a/internal/schema/activity.go b/internal/schema/activity.go new file mode 100644 index 000000000..e62bdd780 --- /dev/null +++ b/internal/schema/activity.go @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "github.com/apache/answer/internal/base/constant" + +// ActivityMsg activity message +type ActivityMsg struct { + UserID string + TriggerUserID int64 + ObjectID string + OriginalObjectID string + ActivityTypeKey constant.ActivityTypeKey + RevisionID string + ExtraInfo map[string]string +} + +// GetObjectTimelineReq get object timeline request +type GetObjectTimelineReq struct { + ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"` + ShowVote bool `validate:"omitempty" form:"show_vote"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +// GetObjectTimelineResp get object timeline response +type GetObjectTimelineResp struct { + ObjectInfo *ActObjectInfo `json:"object_info"` + Timeline []*ActObjectTimeline `json:"timeline"` +} + +// ActObjectTimeline act object timeline +type ActObjectTimeline struct { + ActivityID string `json:"activity_id"` + RevisionID string `json:"revision_id"` + CreatedAt int64 `json:"created_at"` + ActivityType string `json:"activity_type"` + Comment string `json:"comment"` + ObjectID string `json:"object_id"` + ObjectType string `json:"object_type"` + Cancelled bool `json:"cancelled"` + CancelledAt int64 `json:"cancelled_at"` + UserInfo *UserBasicInfo `json:"user_info,omitempty"` +} + +// ActObjectInfo act object info +type ActObjectInfo struct { + Title string `json:"title"` + ObjectType string `json:"object_type"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + MainTagSlugName string `json:"main_tag_slug_name"` +} + +// GetObjectTimelineDetailReq get object timeline detail request +type GetObjectTimelineDetailReq struct { + NewRevisionID string `validate:"required,gt=0,lte=100" form:"new_revision_id"` + OldRevisionID string `validate:"required,gt=0,lte=100" form:"old_revision_id"` + UserID string `json:"-"` +} + +// GetObjectTimelineDetailResp get object timeline detail response +type GetObjectTimelineDetailResp struct { + NewRevision *ObjectTimelineDetail `json:"new_revision"` + OldRevision *ObjectTimelineDetail `json:"old_revision"` +} + +// ObjectTimelineDetail object timeline detail +type ObjectTimelineDetail struct { + Title string `json:"title"` + Tags []*ObjectTimelineTag `json:"tags"` + OriginalText string `json:"original_text"` + SlugName string `json:"slug_name"` + MainTagSlugName string `json:"main_tag_slug_name"` +} + +// ObjectTimelineTag object timeline tags +type ObjectTimelineTag struct { + SlugName string `json:"slug_name"` + DisplayName string `json:"display_name"` + MainTagSlugName string `json:"main_tag_slug_name"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` +} + +// PassReviewActivity pass review activity +type PassReviewActivity struct { + UserID string `json:"user_id"` + TriggerUserID string `json:"trigger_user_id"` + ObjectID string `json:"object_id"` + OriginalObjectID string `json:"original_object_id"` + RevisionID string `json:"revision_id"` +} diff --git a/internal/schema/answer_activity_schema.go b/internal/schema/answer_activity_schema.go new file mode 100644 index 000000000..be17ac733 --- /dev/null +++ b/internal/schema/answer_activity_schema.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +// AcceptAnswerOperationInfo accept answer operation info +type AcceptAnswerOperationInfo struct { + TriggerUserID string + QuestionObjectID string + QuestionUserID string + AnswerObjectID string + AnswerUserID string + + // vote activity info + Activities []*AcceptAnswerActivity +} + +// AcceptAnswerActivity accept answer activity +type AcceptAnswerActivity struct { + ActivityType int + ActivityUserID string + TriggerUserID string + OriginalObjectID string + Rank int +} + +func (v *AcceptAnswerActivity) HasRank() int { + if v.Rank != 0 { + return 1 + } + return 0 +} + +func (a *AcceptAnswerOperationInfo) GetUserIDs() (userIDs []string) { + for _, act := range a.Activities { + userIDs = append(userIDs, act.ActivityUserID) + } + return userIDs +} diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index fde11165a..015e26ac5 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -1,58 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/pkg/converter" + "github.com/segmentfault/pacman/errors" +) + // RemoveAnswerReq delete answer request type RemoveAnswerReq struct { - // answer id - ID string `validate:"required" json:"id"` - // user id - UserID string `json:"-"` + ID string `validate:"required" json:"id"` + UserID string `json:"-"` + CanDelete bool `json:"-"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` +} + +// RecoverAnswerReq recover answer request +type RecoverAnswerReq struct { + AnswerID string `validate:"required" json:"answer_id"` + UserID string `json:"-"` } const ( - Answer_Adopted_Failed = 1 - Answer_Adopted_Enable = 2 + AnswerAcceptedFailed = 1 + AnswerAcceptedEnable = 2 ) type AnswerAddReq struct { - QuestionId string `json:"question_id" ` // question_id - Content string `json:"content" ` // content - Html string `json:"html" ` // html - UserID string `json:"-" ` // user_id + QuestionID string `json:"question_id"` + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + HTML string `json:"-"` + UserID string `json:"-"` + CanEdit bool `json:"-"` + CanDelete bool `json:"-"` + CanRecover bool `json:"-"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` + IP string `json:"-"` + UserAgent string `json:"-"` +} + +func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }), errors.BadRequest(reason.AnswerContentCannotEmpty) + } + return nil, nil +} + +type GetAnswerInfoResp struct { + Info *AnswerInfo `json:"info"` + Question *QuestionInfoResp `json:"question"` } type AnswerUpdateReq struct { - ID string `json:"id"` // id - QuestionId string `json:"question_id" ` // question_id - UserID string `json:"-" ` // user_id - Title string `json:"title" ` // title - Content string `json:"content"` // content - Html string `json:"html" ` // html - EditSummary string `validate:"omitempty" json:"edit_summary"` //edit_summary + ID string `json:"id"` + QuestionID string `json:"question_id"` + Title string `json:"title"` + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + EditSummary string `validate:"omitempty" json:"edit_summary"` + HTML string `json:"-"` + UserID string `json:"-"` + NoNeedReview bool `json:"-"` + CanEdit bool `json:"-"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` } -type AnswerList struct { - QuestionId string `json:"question_id" form:"question_id"` // question_id - Order string `json:"order" form:"order"` // 1 Default 2 time - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - LoginUserID string `json:"-" ` +func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }), errors.BadRequest(reason.AnswerContentCannotEmpty) + } + return nil, nil +} + +// AnswerUpdateResp answer update resp +type AnswerUpdateResp struct { + WaitForReview bool `json:"wait_for_review"` +} + +type AnswerListReq struct { + QuestionID string `json:"question_id" form:"question_id"` + Order string `json:"order" form:"order"` + Page int `json:"page" form:"page"` + PageSize int `json:"page_size" form:"page_size"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` + CanEdit bool `json:"-"` + CanDelete bool `json:"-"` + CanRecover bool `json:"-"` } type AnswerInfo struct { - ID string `json:"id" xorm:"id"` // id - QuestionId string `json:"question_id" xorm:"question_id"` // question_id - Content string `json:"content" xorm:"content"` // content - Html string `json:"html" xorm:"html"` // html - CreateTime int64 `json:"create_time" xorm:"created"` // create_time - UpdateTime int64 `json:"update_time" xorm:"updated"` // update_time - Adopted int `json:"adopted"` // 1 Failed 2 Adopted - UserId string `json:"-" ` - UserInfo *UserBasicInfo `json:"user_info,omitempty"` - UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` - Collected bool `json:"collected"` - VoteStatus string `json:"vote_status"` - VoteCount int `json:"vote_count"` - QuestionInfo *QuestionInfo `json:"question_info,omitempty"` + ID string `json:"id"` + QuestionID string `json:"question_id"` + Content string `json:"content"` + HTML string `json:"html"` + CreateTime int64 `json:"create_time"` + UpdateTime int64 `json:"update_time"` + Accepted int `json:"accepted"` + UserID string `json:"-"` + UpdateUserID string `json:"-"` + UserInfo *UserBasicInfo `json:"user_info,omitempty"` + UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` + Collected bool `json:"collected"` + VoteStatus string `json:"vote_status"` + VoteCount int `json:"vote_count"` + QuestionInfo *QuestionInfoResp `json:"question_info,omitempty"` + Status int `json:"status"` // MemberActions MemberActions []*PermissionMemberAction `json:"member_actions"` @@ -60,12 +142,13 @@ type AnswerInfo struct { type AdminAnswerInfo struct { ID string `json:"id"` - QuestionId string `json:"question_id"` + QuestionID string `json:"question_id"` Description string `json:"description"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` - Adopted int `json:"adopted"` - UserId string `json:"-" ` + Accepted int `json:"accepted"` + UserID string `json:"-"` + UpdateUserID string `json:"-"` UserInfo *UserBasicInfo `json:"user_info"` VoteCount int `json:"vote_count"` QuestionInfo struct { @@ -73,8 +156,21 @@ type AdminAnswerInfo struct { } `json:"question_info"` } -type AnswerAdoptedReq struct { - QuestionID string `json:"question_id" ` // question_id - AnswerID string `json:"answer_id" ` - UserID string `json:"-" ` +type AcceptAnswerReq struct { + QuestionID string `validate:"required,gt=0,lte=30" json:"question_id"` + AnswerID string `validate:"omitempty" json:"answer_id"` + UserID string `json:"-"` +} + +func (req *AcceptAnswerReq) Check() (errFields []*validator.FormErrorField, err error) { + if len(req.AnswerID) == 0 { + req.AnswerID = "0" + } + return nil, nil +} + +type AdminUpdateAnswerStatusReq struct { + AnswerID string `validate:"required" json:"answer_id"` + Status string `validate:"required,oneof=available deleted" json:"status"` + UserID string `json:"-"` } diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index bccf1b63a..6ec848721 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -1,24 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "context" + "strings" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/errors" +) + // UpdateUserStatusReq update user request type UpdateUserStatusReq struct { - // user id - UserID string `validate:"required" json:"user_id"` - // user status - Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"` + UserID string `validate:"required" json:"user_id"` + Status string `validate:"required,oneof=normal suspended deleted inactive" json:"status" enums:"normal,suspended,deleted,inactive"` + SuspendDuration string `validate:"omitempty,oneof=24h 48h 72h 7d 14d 1m 2m 3m 6m 1y forever" json:"suspend_duration"` + RemoveAllContent bool `validate:"omitempty" json:"remove_all_content"` + LoginUserID string `json:"-"` } -const ( - UserNormal = "normal" - UserSuspended = "suspended" - UserDeleted = "deleted" - UserInactive = "inactive" -) +func (r *UpdateUserStatusReq) IsNormal() bool { return r.Status == constant.UserNormal } +func (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == constant.UserSuspended } +func (r *UpdateUserStatusReq) IsDeleted() bool { return r.Status == constant.UserDeleted } +func (r *UpdateUserStatusReq) IsInactive() bool { return r.Status == constant.UserInactive } -func (r *UpdateUserStatusReq) IsNormal() bool { return r.Status == UserNormal } -func (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == UserSuspended } -func (r *UpdateUserStatusReq) IsDeleted() bool { return r.Status == UserDeleted } -func (r *UpdateUserStatusReq) IsInactive() bool { return r.Status == UserInactive } +// GetSuspendedUntil calculates the suspended until time based on duration +func (r *UpdateUserStatusReq) GetSuspendedUntil() time.Time { + if !r.IsSuspended() || r.SuspendDuration == "" || r.SuspendDuration == "forever" { + return entity.PermanentSuspensionTime // permanent suspension + } + + now := time.Now() + switch r.SuspendDuration { + case "24h": + return now.Add(24 * time.Hour) + case "48h": + return now.Add(48 * time.Hour) + case "72h": + return now.Add(72 * time.Hour) + case "7d": + return now.Add(7 * 24 * time.Hour) + case "14d": + return now.Add(14 * 24 * time.Hour) + case "1m": + return now.AddDate(0, 1, 0) + case "2m": + return now.AddDate(0, 2, 0) + case "3m": + return now.AddDate(0, 3, 0) + case "6m": + return now.AddDate(0, 6, 0) + case "1y": + return now.AddDate(1, 0, 0) + default: + return entity.PermanentSuspensionTime // fallback to permanent + } +} // GetUserPageReq get user list page request type GetUserPageReq struct { @@ -26,17 +86,17 @@ type GetUserPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` - // username - Username string `validate:"omitempty,gt=0,lte=50" form:"username"` // email - EMail string `validate:"omitempty,gt=0,lte=100" form:"e_mail"` + Query string `validate:"omitempty,gt=0,lte=100" form:"query"` // user status - Status string `validate:"omitempty,oneof=suspended deleted inactive" form:"status"` + Status string `validate:"omitempty,oneof=normal suspended deleted inactive" form:"status"` + // staff, if staff is true means query admin or moderator + Staff bool `validate:"omitempty" form:"staff"` } -func (r *GetUserPageReq) IsSuspended() bool { return r.Status == UserSuspended } -func (r *GetUserPageReq) IsDeleted() bool { return r.Status == UserDeleted } -func (r *GetUserPageReq) IsInactive() bool { return r.Status == UserInactive } +func (r *GetUserPageReq) IsSuspended() bool { return r.Status == constant.UserSuspended } +func (r *GetUserPageReq) IsDeleted() bool { return r.Status == constant.UserDeleted } +func (r *GetUserPageReq) IsInactive() bool { return r.Status == constant.UserInactive } // GetUserPageResp get user response type GetUserPageResp struct { @@ -48,6 +108,8 @@ type GetUserPageResp struct { DeletedAt int64 `json:"deleted_at"` // suspended time SuspendedAt int64 `json:"suspended_at"` + // suspended until time + SuspendedUntil int64 `json:"suspended_until"` // username Username string `json:"username"` // email @@ -60,6 +122,10 @@ type GetUserPageResp struct { DisplayName string `json:"display_name"` // avatar Avatar string `json:"avatar"` + // role id + RoleID int `json:"role_id"` + // role name + RoleName string `json:"role_name"` } // GetUserInfoReq get user request @@ -69,4 +135,124 @@ type GetUserInfoReq struct { // GetUserInfoResp get user response type GetUserInfoResp struct { + // suspended until + SuspendedUntil time.Time `json:"suspended_until"` +} + +// UpdateUserRoleReq update user role request +type UpdateUserRoleReq struct { + // user id + UserID string `validate:"required" json:"user_id"` + // role id + RoleID int `validate:"required" json:"role_id"` + // login user id + LoginUserID string `json:"-"` +} + +// EditUserProfileReq edit user profile request +type EditUserProfileReq struct { + UserID string `validate:"required" json:"user_id"` + DisplayName string `validate:"required,gte=2,lte=30" json:"display_name"` + Username string `validate:"omitempty,gte=2,lte=30" json:"username"` + Email string `validate:"required,email,gt=0,lte=500" json:"email"` + LoginUserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +// AddUserReq add user request +type AddUserReq struct { + DisplayName string `validate:"required,gte=2,lte=30" json:"display_name"` + Email string `validate:"required,email,gt=0,lte=500" json:"email"` + Password string `validate:"required,gte=8,lte=32" json:"password"` + LoginUserID string `json:"-"` +} + +// AddUsersReq add users request +type AddUsersReq struct { + // users info line by line + UsersStr string `json:"users"` + Users []*AddUserReq `json:"-"` +} + +// DeletePermanentlyReq delete permanently request +type DeletePermanentlyReq struct { + Type string `validate:"required,oneof=users questions answers" json:"type"` +} + +type AddUsersErrorData struct { + // optional. error field name. + Field string `json:"field"` + // must. error line number. + Line int `json:"line"` + // must. error content. + Content string `json:"content"` + // optional. error message. + ExtraMessage string `json:"extra_message"` +} + +func (e *AddUsersErrorData) GetErrField(ctx context.Context) (errFields []*validator.FormErrorField) { + return append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "users", + ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError, e), + }) +} + +func (req *AddUsersReq) ParseUsers(ctx context.Context) (errFields []*validator.FormErrorField, err error) { + req.UsersStr = strings.TrimSpace(req.UsersStr) + lines := strings.Split(req.UsersStr, "\n") + req.Users = make([]*AddUserReq, 0) + for i, line := range lines { + arr := strings.Split(line, ",") + if len(arr) != 3 { + errFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "users", + ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersFormatError, + &AddUsersErrorData{ + Line: i + 1, + Content: line, + }), + }) + return errFields, errors.BadRequest(reason.RequestFormatError) + } + req.Users = append(req.Users, &AddUserReq{ + DisplayName: strings.TrimSpace(arr[0]), + Email: strings.TrimSpace(arr[1]), + Password: strings.TrimSpace(arr[2]), + }) + } + + // check users amount + if len(req.Users) <= 0 || len(req.Users) > constant.DefaultBulkUser { + errFields = append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "users", + ErrorMsg: translator.TrWithData(handler.GetLangByCtx(ctx), reason.AddBulkUsersAmountError, + map[string]int{ + "MaxAmount": constant.DefaultBulkUser, + }), + }) + return errFields, errors.BadRequest(reason.RequestFormatError) + } + return nil, nil +} + +// UpdateUserPasswordReq update user password request +type UpdateUserPasswordReq struct { + UserID string `validate:"required" json:"user_id"` + Password string `validate:"required,gte=8,lte=32" json:"password"` + LoginUserID string `json:"-"` +} + +// GetUserActivationReq get user activation +type GetUserActivationReq struct { + UserID string `validate:"required" form:"user_id"` +} + +// GetUserActivationResp get user activation +type GetUserActivationResp struct { + ActivationURL string `json:"activation_url"` +} + +// SendUserActivationReq send user activation +type SendUserActivationReq struct { + UserID string `validate:"required" json:"user_id"` } diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go new file mode 100644 index 000000000..86d13c03a --- /dev/null +++ b/internal/schema/badge_schema.go @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "github.com/apache/answer/internal/entity" + +const ( + BadgeStatusActive BadgeStatus = "active" + BadgeStatusInactive BadgeStatus = "inactive" +) + +type BadgeStatus string + +var BadgeStatusMap = map[int8]BadgeStatus{ + entity.BadgeStatusActive: BadgeStatusActive, + entity.BadgeStatusInactive: BadgeStatusInactive, +} + +var BadgeStatusEMap = map[BadgeStatus]int8{ + BadgeStatusActive: entity.BadgeStatusActive, + BadgeStatusInactive: entity.BadgeStatusInactive, +} + +// BadgeListInfo get badge list response +type BadgeListInfo struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + EarnedCount int64 `json:"earned_count" ` + // badge level + Level entity.BadgeLevel `json:"level" ` +} + +type GetBadgeListResp struct { + // badge list info + Badges []*BadgeListInfo `json:"badges" ` + // badge group name + GroupName string `json:"group_name" ` +} + +type UpdateBadgeStatusReq struct { + // badge id + ID string `validate:"required" json:"id"` + // badge status + Status BadgeStatus `validate:"required" json:"status"` +} + +type GetBadgeListPagedReq struct { + // page + Page int `validate:"omitempty,min=1" form:"page"` + // page size + PageSize int `validate:"omitempty,min=1" form:"page_size"` + // badge status + Status BadgeStatus `validate:"omitempty" form:"status"` + // query condition + Query string `validate:"omitempty" form:"q"` +} + +type GetBadgeListPagedResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + Earned bool `json:"earned" ` + // badge level + Level entity.BadgeLevel `json:"level" ` + // badge group name + GroupName string `json:"group_name" ` + // badge status + Status BadgeStatus `json:"status"` +} + +type GetBadgeInfoResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + EarnedCount int64 `json:"earned_count" ` + // badge is single or multiple + IsSingle bool `json:"is_single" ` + // badge level + Level entity.BadgeLevel `json:"level" ` +} + +type GetBadgeAwardWithPageReq struct { + // page + Page int `validate:"omitempty,min=1" form:"page"` + // page size + PageSize int `validate:"omitempty,min=1" form:"page_size"` + // badge id + BadgeID string `validate:"required" form:"badge_id"` + // username + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` + // user id + UserID string `json:"-"` +} + +type GetBadgeAwardWithPageResp struct { + // created time + CreatedAt int64 `json:"created_at"` + // object id + ObjectID string `json:"object_id"` + // question id + QuestionID string `json:"question_id"` + // answer id + AnswerID string `json:"answer_id"` + // comment id + CommentID string `json:"comment_id"` + // object type + ObjectType string `json:"object_type" enums:"question,answer,comment"` + // url title + UrlTitle string `json:"url_title"` + // author user info + AuthorUserInfo UserBasicInfo `json:"author_user_info"` +} + +type GetUserBadgeAwardListReq struct { + // username + Username string `validate:"required,gt=0,lte=100" form:"username"` + // user id + UserID string `json:"-"` + Limit int `json:"-"` +} + +type GetUserBadgeAwardListResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + EarnedCount int64 `json:"earned_count" ` + // badge level + Level entity.BadgeLevel `json:"level" ` +} + +type BadgeTplData struct { + ProfileURL string +} diff --git a/internal/schema/collection_group_schema.go b/internal/schema/collection_group_schema.go index 19d6f4d5c..3177f3f15 100644 --- a/internal/schema/collection_group_schema.go +++ b/internal/schema/collection_group_schema.go @@ -1,32 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema import "time" const ( - CG_DEFAULT = 1 - CG_DIY = 2 + CGDefault = 1 + CGDIY = 2 ) // CollectionSwitchReq switch collection request type CollectionSwitchReq struct { - // object TagID ObjectID string `validate:"required" json:"object_id"` - // user collection group TagID - GroupID string `validate:"required" json:"group_id"` -} - -// CollectionSwitchDTO collection data transfer object -type CollectionSwitchDTO struct { - ObjectID string - GroupID string - UserID string + GroupID string `validate:"required" json:"group_id"` + Bookmark bool `validate:"omitempty" json:"bookmark"` + UserID string `json:"-"` } // CollectionSwitchResp switch collection response type CollectionSwitchResp struct { - ObjectID string `json:"object_id"` - Switch bool `json:"switch"` - ObjectCollectionCount string `json:"object_collection_count"` + ObjectCollectionCount int64 `json:"object_collection_count"` } // AddCollectionGroupReq add collection group request diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go index 53ebf445f..104e3f33f 100644 --- a/internal/schema/comment_schema.go +++ b/internal/schema/comment_schema.go @@ -1,8 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema import ( - "github.com/answerdev/answer/internal/entity" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/converter" "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" ) // AddCommentReq add comment request @@ -12,13 +35,33 @@ type AddCommentReq struct { // reply comment id ReplyCommentID string `validate:"omitempty" json:"reply_comment_id"` // original comment content - OriginalText string `validate:"required" json:"original_text"` + OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"` // parsed comment content - ParsedText string `validate:"required" json:"parsed_text"` + ParsedText string `json:"-"` // @ user id list MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` + // user id UserID string `json:"-"` + // whether user can add it + CanAdd bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` +} + +func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) { + req.ParsedText = converter.Markdown2HTML(req.OriginalText) + if req.ParsedText == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "original_text", + ErrorMsg: reason.CommentContentCannotEmpty, + }), errors.BadRequest(reason.CommentContentCannotEmpty) + } + return nil, nil } // RemoveCommentReq remove comment @@ -26,7 +69,9 @@ type RemoveCommentReq struct { // comment id CommentID string `validate:"required" json:"comment_id"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` } // UpdateCommentReq update comment request @@ -34,11 +79,39 @@ type UpdateCommentReq struct { // comment id CommentID string `validate:"required" json:"comment_id"` // original comment content - OriginalText string `validate:"omitempty" json:"original_text"` + OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"` // parsed comment content - ParsedText string `validate:"omitempty" json:"parsed_text"` + ParsedText string `json:"-"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` + + // whether user can edit it + CanEdit bool `json:"-"` + + // whether user can delete it + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` +} + +func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) { + req.ParsedText = converter.Markdown2HTML(req.OriginalText) + if req.ParsedText == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "original_text", + ErrorMsg: reason.CommentContentCannotEmpty, + }), errors.BadRequest(reason.CommentContentCannotEmpty) + } + return nil, nil +} + +type UpdateCommentResp struct { + // comment id + CommentID string `json:"comment_id"` + // original comment content + OriginalText string `json:"original_text"` + // parsed comment content + ParsedText string `json:"parsed_text"` } // GetCommentListReq get comment list all request @@ -69,10 +142,16 @@ type GetCommentWithPageReq struct { PageSize int `validate:"omitempty,min=1" form:"page_size"` // object id ObjectID string `validate:"required" form:"object_id"` + // comment id + CommentID string `validate:"omitempty" form:"comment_id"` // query condition - QueryCond string `validate:"omitempty,oneof=vote" form:"query_cond"` + QueryCond string `validate:"omitempty,oneof=vote created_at" form:"query_cond"` // user id UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } // GetCommentReq get comment list page request @@ -81,6 +160,10 @@ type GetCommentReq struct { ID string `validate:"required" form:"id"` // user id UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` } // GetCommentResp comment response @@ -163,6 +246,8 @@ type GetCommentPersonalWithPageResp struct { ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` + // url title + UrlTitle string `json:"url_title"` // content Content string `json:"content"` } diff --git a/internal/schema/config_schema.go b/internal/schema/config_schema.go index 3a097a4fb..2c660e877 100644 --- a/internal/schema/config_schema.go +++ b/internal/schema/config_schema.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema // AddConfigReq add config request diff --git a/internal/schema/connector_schema.go b/internal/schema/connector_schema.go new file mode 100644 index 000000000..6295c65f7 --- /dev/null +++ b/internal/schema/connector_schema.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +type ConnectorInfoResp struct { + Name string `json:"name"` + Icon string `json:"icon"` + Link string `json:"link"` +} + +type ConnectorUserInfoResp struct { + Name string `json:"name"` + Icon string `json:"icon"` + Link string `json:"link"` + Binding bool `json:"binding"` + ExternalID string `json:"external_id"` +} diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go new file mode 100644 index 000000000..c2c7677d0 --- /dev/null +++ b/internal/schema/dashboard_schema.go @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "time" + +var AppStartTime time.Time + +const ( + DashboardCacheKey = "answer:dashboard" + DashboardCacheTime = 60 * time.Minute +) + +type DashboardInfo struct { + QuestionCount int64 `json:"question_count"` + ResolvedCount int64 `json:"resolved_count"` + ResolvedRate string `json:"resolved_rate"` + UnansweredCount int64 `json:"unanswered_count"` + UnansweredRate string `json:"unanswered_rate"` + AnswerCount int64 `json:"answer_count"` + CommentCount int64 `json:"comment_count"` + VoteCount int64 `json:"vote_count"` + UserCount int64 `json:"user_count"` + ReportCount int64 `json:"report_count"` + UploadingFiles bool `json:"uploading_files"` + SMTP string `json:"smtp"` + HTTPS bool `json:"https"` + TimeZone string `json:"time_zone"` + OccupyingStorageSpace string `json:"occupying_storage_space"` + AppStartTime string `json:"app_start_time"` + VersionInfo DashboardInfoVersion `json:"version_info"` + LoginRequired bool `json:"login_required"` + GoVersion string `json:"go_version"` + DatabaseVersion string `json:"database_version"` + DatabaseSize string `json:"database_size"` +} + +type DashboardInfoVersion struct { + Version string `json:"version"` + Revision string `json:"revision"` + RemoteVersion string `json:"remote_version"` +} + +type RemoteVersion struct { + Release struct { + Version string `json:"version"` + URL string `json:"url"` + } `json:"release"` +} diff --git a/internal/schema/email_template.go b/internal/schema/email_template.go new file mode 100644 index 000000000..1fcdfbce3 --- /dev/null +++ b/internal/schema/email_template.go @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "encoding/json" + "github.com/apache/answer/internal/base/constant" +) + +const ( + AccountActivationSourceType EmailSourceType = "account-activation" + PasswordResetSourceType EmailSourceType = "password-reset" + ConfirmNewEmailSourceType EmailSourceType = "password-reset" + UnsubscribeSourceType EmailSourceType = "unsubscribe" + BindingSourceType EmailSourceType = "binding" +) + +type EmailSourceType string + +type EmailCodeContent struct { + SourceType EmailSourceType `json:"source_type"` + Email string `json:"e_mail"` + UserID string `json:"user_id"` + // Used for unsubscribe notification + NotificationSources []constant.NotificationSource `json:"notification_source,omitempty"` + // Used for third-party login account binding + BindingKey string `json:"binding_key,omitempty"` + // Skip the validation of the latest code + SkipValidationLatestCode bool `json:"skip_validation_latest_code"` +} + +func (r *EmailCodeContent) ToJSONString() string { + codeBytes, _ := json.Marshal(r) + return string(codeBytes) +} + +func (r *EmailCodeContent) FromJSONString(data string) error { + return json.Unmarshal([]byte(data), &r) +} + +type RegisterTemplateData struct { + SiteName string + RegisterUrl string +} + +type PassResetTemplateData struct { + SiteName string + PassResetUrl string +} + +type ChangeEmailTemplateData struct { + SiteName string + ChangeEmailUrl string +} + +type TestTemplateData struct { + SiteName string +} + +type NewAnswerTemplateRawData struct { + AnswerUserDisplayName string + QuestionTitle string + QuestionID string + AnswerID string + AnswerSummary string + UnsubscribeCode string +} + +type NewAnswerTemplateData struct { + SiteName string + DisplayName string + QuestionTitle string + AnswerUrl string + AnswerSummary string + UnsubscribeUrl string +} + +type NewInviteAnswerTemplateRawData struct { + InviterDisplayName string + QuestionTitle string + QuestionID string + UnsubscribeCode string +} + +type NewInviteAnswerTemplateData struct { + SiteName string + DisplayName string + QuestionTitle string + InviteUrl string + UnsubscribeUrl string +} + +type NewCommentTemplateRawData struct { + CommentUserDisplayName string + QuestionTitle string + QuestionID string + AnswerID string + CommentID string + CommentSummary string + UnsubscribeCode string +} + +type NewCommentTemplateData struct { + SiteName string + DisplayName string + QuestionTitle string + CommentUrl string + CommentSummary string + UnsubscribeUrl string +} + +type NewQuestionTemplateRawData struct { + QuestionAuthorUserID string + QuestionTitle string + QuestionID string + UnsubscribeCode string + Tags []string + TagIDs []string +} + +type NewQuestionTemplateData struct { + SiteName string + QuestionTitle string + QuestionUrl string + Tags string + UnsubscribeUrl string +} diff --git a/internal/schema/err_schema.go b/internal/schema/err_schema.go index 3270b365d..0e0d12003 100644 --- a/internal/schema/err_schema.go +++ b/internal/schema/err_schema.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema type ErrTypeData struct { @@ -7,3 +26,5 @@ type ErrTypeData struct { var ErrTypeModal = ErrTypeData{ErrType: "modal"} var ErrTypeToast = ErrTypeData{ErrType: "toast"} + +var ErrTypeAlert = ErrTypeData{ErrType: "alert"} diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go new file mode 100644 index 000000000..fabbabce4 --- /dev/null +++ b/internal/schema/event_schema.go @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/pkg/uid" +) + +// EventMsg event message +type EventMsg struct { + EventType constant.EventType + UserID string + + TriggerObjectID string + + QuestionID string + QuestionUserID string + + AnswerID string + AnswerUserID string + + CommentID string + CommentUserID string + + ExtraInfo map[string]string +} + +// NewEvent create a new event +func NewEvent(e constant.EventType, userID string) *EventMsg { + return &EventMsg{ + UserID: userID, + EventType: e, + ExtraInfo: make(map[string]string), + } +} + +// QID get question id +func (e *EventMsg) QID(questionID, userID string) *EventMsg { + if len(questionID) > 0 { + e.QuestionID = uid.DeShortID(questionID) + } + e.QuestionUserID = userID + return e +} + +// AID get answer id +func (e *EventMsg) AID(answerID, userID string) *EventMsg { + if len(answerID) > 0 { + e.AnswerID = uid.DeShortID(answerID) + } + e.AnswerUserID = userID + return e +} + +// CID get comment id +func (e *EventMsg) CID(comment, userID string) *EventMsg { + e.CommentID = comment + e.CommentUserID = userID + return e +} + +// TID get trigger object id +func (e *EventMsg) TID(triggerObjectID string) *EventMsg { + if len(triggerObjectID) > 0 { + e.TriggerObjectID = uid.DeShortID(triggerObjectID) + } + return e +} + +// AddExtra add extra info +func (e *EventMsg) AddExtra(key, value string) *EventMsg { + e.ExtraInfo[key] = value + return e +} + +// GetExtra get extra info +func (e *EventMsg) GetExtra(key string) string { + if v, ok := e.ExtraInfo[key]; ok { + return v + } + return "" +} + +// GetObjectID get object id +func (e *EventMsg) GetObjectID() string { + if len(e.TriggerObjectID) > 0 { + return e.TriggerObjectID + } + if len(e.CommentID) > 0 { + return e.CommentID + } + if len(e.AnswerID) > 0 { + return e.AnswerID + } + return e.QuestionID +} diff --git a/internal/schema/follow_schema.go b/internal/schema/follow_schema.go index c6f8e8028..17d704223 100644 --- a/internal/schema/follow_schema.go +++ b/internal/schema/follow_schema.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema // FollowReq follow object request diff --git a/internal/schema/forbidden_schema.go b/internal/schema/forbidden_schema.go index 7ac7c5652..ae699195d 100644 --- a/internal/schema/forbidden_schema.go +++ b/internal/schema/forbidden_schema.go @@ -1,8 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema const ( ForbiddenReasonTypeInactive = "inactive" - ForbiddenReasonTypeUrlExpired = "url_expired" + ForbiddenReasonTypeURLExpired = "url_expired" ForbiddenReasonTypeUserSuspended = "suspended" ) diff --git a/internal/schema/lang_schema.go b/internal/schema/lang_schema.go deleted file mode 100644 index 98abe8742..000000000 --- a/internal/schema/lang_schema.go +++ /dev/null @@ -1,18 +0,0 @@ -package schema - -// GetLangOption get label option -type GetLangOption struct { - Label string `json:"label"` - Value string `json:"value"` -} - -var GetLangOptions = []*GetLangOption{ - { - Label: "English(US)", - Value: "en_US", - }, - { - Label: "中文(CN)", - Value: "zh_CN", - }, -} diff --git a/internal/schema/meta_schema.go b/internal/schema/meta_schema.go new file mode 100644 index 000000000..286e2e7d6 --- /dev/null +++ b/internal/schema/meta_schema.go @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +type UpdateReactionReq struct { + ObjectID string `validate:"required" json:"object_id"` + Emoji string `validate:"required,oneof=heart smile frown" json:"emoji"` + Reaction string `validate:"required,oneof=activate deactivate" json:"reaction"` + UserID string `json:"-"` +} + +type GetReactionReq struct { + ObjectID string `validate:"required" form:"object_id"` + UserID string `json:"-"` +} + +// ReactionsSummaryMeta reactions summary meta +type ReactionsSummaryMeta struct { + Reactions []*ReactionSummaryMeta `json:"reactions"` +} + +// ReactionSummaryMeta reaction summary meta +type ReactionSummaryMeta struct { + Emoji string `json:"emoji"` + UserIDs []string `json:"user_ids"` +} + +// AddReactionSummary add user operation to reaction summary +func (r *ReactionsSummaryMeta) AddReactionSummary(emoji, userID string) { + for _, reaction := range r.Reactions { + if reaction.Emoji != emoji { + continue + } + exist := false + for _, id := range reaction.UserIDs { + if id == userID { + exist = true + break + } + } + if !exist { + reaction.UserIDs = append(reaction.UserIDs, userID) + } + return + } + r.Reactions = append(r.Reactions, &ReactionSummaryMeta{ + Emoji: emoji, + UserIDs: []string{userID}, + }) +} + +// RemoveReactionSummary remove user operation from reaction summary +func (r *ReactionsSummaryMeta) RemoveReactionSummary(emoji, userID string) { + updatedReactions := make([]*ReactionSummaryMeta, 0) + for _, reaction := range r.Reactions { + if reaction.Emoji != emoji && len(reaction.UserIDs) > 0 { + updatedReactions = append(updatedReactions, reaction) + continue + } + updatedUserIDs := make([]string, 0, len(r.Reactions)) + for _, id := range reaction.UserIDs { + if id != userID { + updatedUserIDs = append(updatedUserIDs, id) + } + } + if len(updatedUserIDs) > 0 { + reaction.UserIDs = updatedUserIDs + updatedReactions = append(updatedReactions, reaction) + } + } + r.Reactions = updatedReactions +} + +// CheckUserInReactionSummary check user's operation if in reaction summary +func (r *ReactionsSummaryMeta) CheckUserInReactionSummary(emoji, userID string) bool { + for _, reaction := range r.Reactions { + if reaction.Emoji != emoji { + continue + } + for _, id := range reaction.UserIDs { + if id == userID { + return true + } + } + } + return false +} + +// GetReactionByObjectIdResp get reaction by object id response +type GetReactionByObjectIdResp struct { + ReactionSummary []*ReactionRespItem `json:"reaction_summary"` +} + +// ReactionRespItem reaction response item +type ReactionRespItem struct { + // Emoji is the reaction emoji + Emoji string `json:"emoji"` + // Count is the number of users who reacted + Count int `json:"count"` + // Tooltip is the user's name who reacted + Tooltip string `json:"tooltip"` + // IsActive is if current user has reacted + IsActive bool `json:"is_active"` +} diff --git a/internal/schema/new_question_queue_schema.go b/internal/schema/new_question_queue_schema.go new file mode 100644 index 000000000..5faeabb2d --- /dev/null +++ b/internal/schema/new_question_queue_schema.go @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/uid" +) + +type ExternalNotificationMsg struct { + ReceiverUserID string `json:"receiver_user_id"` + ReceiverEmail string `json:"receiver_email"` + ReceiverLang string `json:"receiver_lang"` + + NewAnswerTemplateRawData *NewAnswerTemplateRawData `json:"new_answer_template_raw_data,omitempty"` + NewInviteAnswerTemplateRawData *NewInviteAnswerTemplateRawData `json:"new_invite_answer_template_raw_data,omitempty"` + NewCommentTemplateRawData *NewCommentTemplateRawData `json:"new_comment_template_raw_data,omitempty"` + NewQuestionTemplateRawData *NewQuestionTemplateRawData `json:"new_question_template_raw_data,omitempty"` +} + +func CreateNewQuestionNotificationMsg( + questionID, questionTitle, questionAuthorUserID string, tags []*entity.Tag) *ExternalNotificationMsg { + questionID = uid.DeShortID(questionID) + msg := &ExternalNotificationMsg{ + NewQuestionTemplateRawData: &NewQuestionTemplateRawData{ + QuestionAuthorUserID: questionAuthorUserID, + QuestionID: questionID, + QuestionTitle: questionTitle, + }, + } + for _, tag := range tags { + msg.NewQuestionTemplateRawData.Tags = append(msg.NewQuestionTemplateRawData.Tags, tag.SlugName) + msg.NewQuestionTemplateRawData.TagIDs = append(msg.NewQuestionTemplateRawData.TagIDs, tag.ID) + } + return msg +} diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 6a8f470ff..a68328ace 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -1,12 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "encoding/json" + "github.com/apache/answer/internal/entity" + "sort" +) + const ( - NotificationTypeInbox = 1 - NotificationTypeAchievement = 2 - NotificationNotRead = 1 - NotificationRead = 2 - NotificationStatusNormal = 1 - NotificationStatusDelete = 10 + NotificationTypeInbox = 1 + NotificationTypeAchievement = 2 + NotificationNotRead = 1 + NotificationRead = 2 + NotificationStatusNormal = 1 + NotificationStatusDelete = 10 + NotificationInboxTypeAll = 0 + NotificationInboxTypePosts = 1 + NotificationInboxTypeVotes = 2 + NotificationInboxTypeInvites = 3 ) var NotificationType = map[string]int{ @@ -14,6 +43,13 @@ var NotificationType = map[string]int{ "achievement": NotificationTypeAchievement, } +var NotificationInboxType = map[string]int{ + "all": NotificationInboxTypeAll, + "posts": NotificationInboxTypePosts, + "invites": NotificationInboxTypeInvites, + "votes": NotificationInboxTypeVotes, +} + type NotificationContent struct { ID string `json:"id"` TriggerUserID string `json:"-"` //show userid @@ -27,6 +63,14 @@ type NotificationContent struct { UpdateTime int64 `json:"update_time"` } +type GetRedDot struct { + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + // NotificationMsg notification message type NotificationMsg struct { // trigger notification user id @@ -45,6 +89,8 @@ type NotificationMsg struct { NotificationAction string // if true no need to send notification to all followers NoNeedPushAllFollow bool + // extra info + ExtraInfo map[string]string } type ObjectInfo struct { @@ -55,21 +101,88 @@ type ObjectInfo struct { } type RedDot struct { - Inbox int64 `json:"inbox"` - Achievement int64 `json:"achievement"` + Inbox int64 `json:"inbox"` + Achievement int64 `json:"achievement"` + Revision int64 `json:"revision"` + CanRevision bool `json:"can_revision"` + BadgeAward *RedDotBadgeAward `json:"badge_award"` +} + +type RedDotBadgeAward struct { + NotificationID string `json:"notification_id"` + BadgeID string `json:"badge_id"` + Name string `json:"name"` + Icon string `json:"icon"` + Level entity.BadgeLevel `json:"level"` +} + +type RedDotBadgeAwardCache struct { + BadgeAwardList map[string]*RedDotBadgeAward `json:"badge_award_list"` +} + +// NewRedDotBadgeAwardCache new red dot badge award cache +func NewRedDotBadgeAwardCache() *RedDotBadgeAwardCache { + return &RedDotBadgeAwardCache{ + BadgeAwardList: make(map[string]*RedDotBadgeAward), + } +} + +// GetBadgeAward get badge award +func (r *RedDotBadgeAwardCache) GetBadgeAward() *RedDotBadgeAward { + if len(r.BadgeAwardList) == 0 { + return nil + } + var ids []string + for _, v := range r.BadgeAwardList { + ids = append(ids, v.NotificationID) + } + sort.Strings(ids) + return r.BadgeAwardList[ids[0]] +} + +// FromJSON from json +func (r *RedDotBadgeAwardCache) FromJSON(data string) { + _ = json.Unmarshal([]byte(data), r) +} + +// ToJSON to json +func (r *RedDotBadgeAwardCache) ToJSON() string { + data, _ := json.Marshal(r) + return string(data) +} + +// AddBadgeAward add badge award +func (r *RedDotBadgeAwardCache) AddBadgeAward(badgeAward *RedDotBadgeAward) { + if r.BadgeAwardList == nil { + r.BadgeAwardList = make(map[string]*RedDotBadgeAward) + } + r.BadgeAwardList[badgeAward.NotificationID] = badgeAward +} + +// RemoveBadgeAward remove badge award +func (r *RedDotBadgeAwardCache) RemoveBadgeAward(notificationID string) { + if r.BadgeAwardList == nil { + return + } + delete(r.BadgeAwardList, notificationID) } type NotificationSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Type int `json:"-" form:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement - UserID string `json:"-"` + Page int `json:"page" form:"page"` //Query number of pages + PageSize int `json:"page_size" form:"page_size"` //Search page size + Type int `json:"-" form:"-"` + TypeStr string `json:"type" form:"type"` // inbox achievement + InboxTypeStr string `json:"inbox_type" form:"inbox_type"` // inbox achievement + InboxType int `json:"-" form:"-"` // inbox achievement + UserID string `json:"-"` } type NotificationClearRequest struct { - UserID string `json:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement + NotificationType string `validate:"required,oneof=inbox achievement" json:"type"` + UserID string `json:"-"` + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` } type NotificationClearIDRequest struct { diff --git a/internal/schema/permission.go b/internal/schema/permission.go index 2652acfae..106709d12 100644 --- a/internal/schema/permission.go +++ b/internal/schema/permission.go @@ -1,7 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema -const PermissionMemberActionTypeEdit = "edit" -const PermissionMemberActionTypeReason = "reason" +import ( + "strings" + + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/segmentfault/pacman/i18n" +) + +// PermissionTrTplData template data as for translate permission message +type PermissionTrTplData struct { + Rank int +} // PermissionMemberAction permission member action type PermissionMemberAction struct { @@ -9,3 +39,35 @@ type PermissionMemberAction struct { Name string `json:"name"` Type string `json:"type"` } + +// GetPermissionReq get permission request +type GetPermissionReq struct { + Action string `form:"action"` + Actions []string `validate:"omitempty" form:"actions"` +} + +func (r *GetPermissionReq) Check() (errField []*validator.FormErrorField, err error) { + if len(r.Action) > 0 { + r.Actions = strings.Split(r.Action, ",") + } + return nil, nil +} + +// GetPermissionResp get permission response +type GetPermissionResp struct { + HasPermission bool `json:"has_permission"` + // only not allow, will return this tip + NoPermissionTip string `json:"no_permission_tip"` +} + +func (r *GetPermissionResp) TrTip(lang i18n.Language, requireRank int) { + if r.HasPermission { + return + } + if requireRank <= 0 { + r.NoPermissionTip = translator.Tr(lang, reason.RankFailToMeetTheCondition) + } else { + r.NoPermissionTip = translator.TrWithData( + lang, reason.NoEnoughRankToOperate, &PermissionTrTplData{Rank: requireRank}) + } +} diff --git a/internal/schema/plugin_admin_schema.go b/internal/schema/plugin_admin_schema.go new file mode 100644 index 000000000..77129ac73 --- /dev/null +++ b/internal/schema/plugin_admin_schema.go @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +const ( + PluginStatusActive PluginStatus = "active" + PluginStatusInactive PluginStatus = "inactive" +) + +type PluginStatus string + +type GetPluginListReq struct { + Status PluginStatus `form:"status"` + HaveConfig bool `form:"have_config"` +} + +type GetPluginListResp struct { + Name string `json:"name"` + SlugName string `json:"slug_name"` + Description string `json:"description"` + Version string `json:"version"` + Enabled bool `json:"enabled"` + HaveConfig bool `json:"have_config"` + Link string `json:"link"` +} + +type GetAllPluginStatusResp struct { + SlugName string `json:"slug_name"` + Enabled bool `json:"enabled"` +} + +type UpdatePluginStatusReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` + Enabled bool `json:"enabled"` +} + +type GetPluginConfigReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"` +} + +type GetPluginConfigResp struct { + Name string `json:"name"` + SlugName string `json:"slug_name"` + Description string `json:"description"` + Version string `json:"version"` + ConfigFields []ConfigField `json:"config_fields"` +} + +func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) { + for _, field := range fields { + configField := ConfigField{ + Name: field.Name, + Type: string(field.Type), + Title: field.Title.Translate(ctx), + Description: field.Description.Translate(ctx), + Required: field.Required, + Value: field.Value, + UIOptions: ConfigFieldUIOptions{ + Rows: field.UIOptions.Rows, + InputType: string(field.UIOptions.InputType), + Variant: field.UIOptions.Variant, + ClassName: field.UIOptions.ClassName, + FieldClassName: field.UIOptions.FieldClassName, + }, + } + configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx) + configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx) + configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx) + if field.UIOptions.Action != nil { + uiOptionAction := &UIOptionAction{ + Url: field.UIOptions.Action.Url, + Method: field.UIOptions.Action.Method, + } + if field.UIOptions.Action.Loading != nil { + uiOptionAction.Loading = &LoadingAction{ + Text: field.UIOptions.Action.Loading.Text.Translate(ctx), + State: string(field.UIOptions.Action.Loading.State), + } + } + if field.UIOptions.Action.OnComplete != nil { + uiOptionAction.OnCompleteAction = &OnCompleteAction{ + ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage, + RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig, + } + } + configField.UIOptions.Action = uiOptionAction + } + + for _, option := range field.Options { + configField.Options = append(configField.Options, ConfigFieldOption{ + Label: option.Label.Translate(ctx), + Value: option.Value, + }) + } + g.ConfigFields = append(g.ConfigFields, configField) + } +} + +type ConfigField struct { + Name string `json:"name"` + Type string `json:"type"` + Title string `json:"title"` + Description string `json:"description"` + Required bool `json:"required"` + Value any `json:"value"` + UIOptions ConfigFieldUIOptions `json:"ui_options"` + Options []ConfigFieldOption `json:"options,omitempty"` +} + +type ConfigFieldUIOptions struct { + Placeholder string `json:"placeholder,omitempty"` + Rows string `json:"rows,omitempty"` + InputType string `json:"input_type,omitempty"` + Label string `json:"label,omitempty"` + Action *UIOptionAction `json:"action,omitempty"` + Variant string `json:"variant,omitempty"` + Text string `json:"text,omitempty"` + ClassName string `json:"class_name,omitempty"` + FieldClassName string `json:"field_class_name,omitempty"` +} + +type ConfigFieldOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +type UIOptionAction struct { + Url string `json:"url"` + Method string `json:"method,omitempty"` + Loading *LoadingAction `json:"loading,omitempty"` + OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"` +} + +type LoadingAction struct { + Text string `json:"text"` + State string `json:"state"` +} + +type OnCompleteAction struct { + ToastReturnMessage bool `json:"toast_return_message"` + RefreshFormConfig bool `json:"refresh_form_config"` +} + +type UpdatePluginConfigReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` + ConfigFields map[string]any `json:"config_fields"` +} diff --git a/internal/schema/plugin_user_center.go b/internal/schema/plugin_user_center.go new file mode 100644 index 000000000..3ce9068fb --- /dev/null +++ b/internal/schema/plugin_user_center.go @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +type UserCenterAgentResp struct { + Enabled bool `json:"enabled"` + AgentInfo *AgentInfo `json:"agent_info"` +} + +type AgentInfo struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Icon string `json:"icon"` + Url string `json:"url"` + LoginRedirectURL string `json:"login_redirect_url"` + SignUpRedirectURL string `json:"sign_up_redirect_url"` + ControlCenterItems []*ControlCenter `json:"control_center"` + EnabledOriginalUserSystem bool `json:"enabled_original_user_system"` +} + +type ControlCenter struct { + Name string `json:"name"` + Label string `json:"label"` + Url string `json:"url"` +} + +type UserCenterPersonalBranding struct { + Enabled bool `json:"enabled"` + PersonalBranding []*PersonalBranding `json:"personal_branding"` +} + +type PersonalBranding struct { + Icon string `json:"icon"` + Name string `json:"name"` + Label string `json:"label"` + Url string `json:"url"` +} diff --git a/internal/schema/plugin_user_schema.go b/internal/schema/plugin_user_schema.go new file mode 100644 index 000000000..2e35f58ad --- /dev/null +++ b/internal/schema/plugin_user_schema.go @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" +) + +type GetUserPluginListResp struct { + Name string `json:"name"` + SlugName string `json:"slug_name"` +} + +type UpdateUserPluginReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` + UserID string `json:"-"` +} + +type GetUserPluginConfigReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"` + UserID string `json:"-"` +} + +type GetUserPluginConfigResp struct { + Name string `json:"name"` + SlugName string `json:"slug_name"` + ConfigFields []*ConfigField `json:"config_fields"` +} + +func (g *GetUserPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) { + for _, field := range fields { + configField := &ConfigField{ + Name: field.Name, + Type: string(field.Type), + Title: field.Title.Translate(ctx), + Description: field.Description.Translate(ctx), + Required: field.Required, + Value: field.Value, + UIOptions: ConfigFieldUIOptions{ + Rows: field.UIOptions.Rows, + InputType: string(field.UIOptions.InputType), + Variant: field.UIOptions.Variant, + ClassName: field.UIOptions.ClassName, + FieldClassName: field.UIOptions.FieldClassName, + }, + } + configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx) + configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx) + configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx) + if field.UIOptions.Action != nil { + uiOptionAction := &UIOptionAction{ + Url: field.UIOptions.Action.Url, + Method: field.UIOptions.Action.Method, + } + if field.UIOptions.Action.Loading != nil { + uiOptionAction.Loading = &LoadingAction{ + Text: field.UIOptions.Action.Loading.Text.Translate(ctx), + State: string(field.UIOptions.Action.Loading.State), + } + } + if field.UIOptions.Action.OnComplete != nil { + uiOptionAction.OnCompleteAction = &OnCompleteAction{ + ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage, + RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig, + } + } + configField.UIOptions.Action = uiOptionAction + } + + for _, option := range field.Options { + configField.Options = append(configField.Options, ConfigFieldOption{ + Label: option.Label.Translate(ctx), + Value: option.Value, + }) + } + g.ConfigFields = append(g.ConfigFields, configField) + } +} + +type UpdateUserPluginConfigReq struct { + PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"` + ConfigFields map[string]any `json:"config_fields"` + UserID string `json:"-"` +} diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index a619c4544..b5e4baa32 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -1,18 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "strings" + "time" + + "github.com/apache/answer/internal/base/reason" + "github.com/segmentfault/pacman/errors" + + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/uid" +) + +const ( + QuestionOperationPin = "pin" + QuestionOperationUnPin = "unpin" + QuestionOperationHide = "hide" + QuestionOperationShow = "show" +) + // RemoveQuestionReq delete question request type RemoveQuestionReq struct { // question id - ID string `validate:"required" comment:"question id" json:"id"` - UserID string `json:"-" ` // user_id - + ID string `validate:"required" json:"id"` + UserID string `json:"-" ` // user_id + IsAdmin bool `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } type CloseQuestionReq struct { - ID string `validate:"required" comment:"question id" json:"id"` - UserId string `json:"-" ` // user_id - CloseType int `json:"close_type" ` // close_type - CloseMsg string `json:"close_msg" ` // close_type + ID string `validate:"required" json:"id"` + CloseType int `json:"close_type"` // close_type + CloseMsg string `json:"close_msg"` // close_type + UserID string `json:"-"` // user_id +} + +type OperationQuestionReq struct { + ID string `validate:"required" json:"id"` + Operation string `json:"operation"` // operation [pin unpin hide show] + UserID string `json:"-"` // user_id + CanPin bool `json:"-"` + CanList bool `json:"-"` } type CloseQuestionMeta struct { @@ -20,84 +69,238 @@ type CloseQuestionMeta struct { CloseMsg string `json:"close_msg"` } +// ReopenQuestionReq reopen question request +type ReopenQuestionReq struct { + QuestionID string `json:"question_id"` + UserID string `json:"-"` +} + type QuestionAdd struct { // question title - Title string `validate:"required,gte=6,lte=150" json:"title"` + Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content - Content string `validate:"required,gte=6,lte=65535" json:"content"` + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` // html - Html string `validate:"required,gte=6,lte=65535" json:"html"` + HTML string `json:"-"` // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // user id UserID string `json:"-"` + QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` + IP string `json:"-"` + UserAgent string `json:"-"` +} + +func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + for _, tag := range req.Tags { + if len(tag.OriginalText) > 0 { + tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) + } + } + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }), errors.BadRequest(reason.QuestionContentCannotEmpty) + } + return nil, nil +} + +type QuestionAddByAnswer struct { + // question title + Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` + // content + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + // html + HTML string `json:"-"` + AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"` + AnswerHTML string `json:"-"` + // tags + Tags []*TagItem `validate:"required,dive" json:"tags"` + // user id + UserID string `json:"-"` + MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"` + QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` + IP string `json:"-"` + UserAgent string `json:"-"` +} + +func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + req.AnswerHTML = converter.Markdown2HTML(req.AnswerContent) + for _, tag := range req.Tags { + if len(tag.OriginalText) > 0 { + tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) + } + } + if req.HTML == "" { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }) + } + if req.AnswerHTML == "" { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "answer_content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }) + } + if req.HTML == "" || req.AnswerHTML == "" { + return errFields, errors.BadRequest(reason.QuestionContentCannotEmpty) + } + return nil, nil +} + +type QuestionPermission struct { + // whether user can add it + CanAdd bool `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` + // whether user can delete it + CanDelete bool `json:"-"` + // whether user can close it + CanClose bool `json:"-"` + // whether user can reopen it + CanReopen bool `json:"-"` + // whether user can pin it + CanPin bool `json:"-"` + CanUnPin bool `json:"-"` + // whether user can hide it + CanHide bool `json:"-"` + CanShow bool `json:"-"` + // whether user can use reserved it + CanUseReservedTag bool `json:"-"` + // whether user can invite other user to answer this question + CanInviteOtherToAnswer bool `json:"-"` + CanAddTag bool `json:"-"` + CanRecover bool `json:"-"` +} + +type CheckCanQuestionUpdate struct { + // question id + ID string `validate:"required" form:"id"` + // user id + UserID string `json:"-"` + IsAdmin bool `json:"-"` } type QuestionUpdate struct { // question id ID string `validate:"required" json:"id"` // question title - Title string `validate:"required,gte=6,lte=150" json:"title"` + Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content - Content string `validate:"required,gte=6,lte=65535" json:"content"` + Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` // html - Html string `validate:"required,gte=6,lte=65535" json:"html"` + HTML string `json:"-"` + InviteUser []string `validate:"omitempty" json:"invite_user"` // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + NoNeedReview bool `json:"-"` + QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` +} + +type QuestionRecoverReq struct { + QuestionID string `validate:"required" json:"question_id"` + UserID string `json:"-"` +} + +type QuestionUpdateInviteUser struct { + ID string `validate:"required" json:"id"` + InviteUser []string `validate:"omitempty" json:"invite_user"` + UserID string `json:"-"` + QuestionPermission + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` +} + +func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { + req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }), errors.BadRequest(reason.QuestionContentCannotEmpty) + } + return nil, nil } type QuestionBaseInfo struct { ID string `json:"id" ` - Title string `json:"title" xorm:"title"` // title - ViewCount int `json:"view_count" xorm:"view_count"` // view count - AnswerCount int `json:"answer_count" xorm:"answer_count"` // answer count - CollectionCount int `json:"collection_count" xorm:"collection_count"` // collection count - FollowCount int `json:"follow_count" xorm:"follow_count"` // follow count + Title string `json:"title"` + UrlTitle string `json:"url_title"` + ViewCount int `json:"view_count"` + AnswerCount int `json:"answer_count"` + CollectionCount int `json:"collection_count"` + FollowCount int `json:"follow_count"` Status string `json:"status"` AcceptedAnswer bool `json:"accepted_answer"` } -type QuestionInfo struct { +type QuestionInfoResp struct { ID string `json:"id" ` - Title string `json:"title" xorm:"title"` // title - Content string `json:"content" xorm:"content"` // content - Html string `json:"html" xorm:"html"` // html - Tags []*TagResp `json:"tags" ` // tags - ViewCount int `json:"view_count" xorm:"view_count"` // view_count - UniqueViewCount int `json:"unique_view_count" xorm:"unique_view_count"` // unique_view_count - VoteCount int `json:"vote_count" xorm:"vote_count"` // vote_count - AnswerCount int `json:"answer_count" xorm:"answer_count"` // answer count - CollectionCount int `json:"collection_count" xorm:"collection_count"` // collection count - FollowCount int `json:"follow_count" xorm:"follow_count"` // follow count - AcceptedAnswerId string `json:"accepted_answer_id" ` // accepted_answer_id - LastAnswerId string `json:"last_answer_id" ` // last_answer_id - CreateTime int64 `json:"create_time" ` // create_time - UpdateTime int64 `json:"-"` // update_time + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Content string `json:"content"` + HTML string `json:"html"` + Description string `json:"description"` + Tags []*TagResp `json:"tags"` + ViewCount int `json:"view_count"` + UniqueViewCount int `json:"unique_view_count"` + VoteCount int `json:"vote_count"` + AnswerCount int `json:"answer_count"` + CollectionCount int `json:"collection_count"` + FollowCount int `json:"follow_count"` + AcceptedAnswerID string `json:"accepted_answer_id"` + LastAnswerID string `json:"last_answer_id"` + CreateTime int64 `json:"create_time"` + UpdateTime int64 `json:"-"` PostUpdateTime int64 `json:"update_time"` QuestionUpdateTime int64 `json:"edit_time"` + Pin int `json:"pin"` + Show int `json:"show"` Status int `json:"status"` Operation *Operation `json:"operation,omitempty"` - UserId string `json:"-" ` + UserID string `json:"-"` + LastEditUserID string `json:"-"` + LastAnsweredUserID string `json:"-"` UserInfo *UserBasicInfo `json:"user_info"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` LastAnsweredUserInfo *UserBasicInfo `json:"last_answered_user_info,omitempty"` Answered bool `json:"answered"` + FirstAnswerId string `json:"first_answer_id"` Collected bool `json:"collected"` VoteStatus string `json:"vote_status"` IsFollowed bool `json:"is_followed"` // MemberActions - MemberActions []*PermissionMemberAction `json:"member_actions"` + MemberActions []*PermissionMemberAction `json:"member_actions"` + ExtendsActions []*PermissionMemberAction `json:"extends_actions"` +} + +// UpdateQuestionResp update question resp +type UpdateQuestionResp struct { + UrlTitle string `json:"url_title"` + WaitForReview bool `json:"wait_for_review"` } type AdminQuestionInfo struct { ID string `json:"id"` Title string `json:"title"` VoteCount int `json:"vote_count"` + Show int `json:"show"` + Pin int `json:"pin"` AnswerCount int `json:"answer_count"` AcceptedAnswerID string `json:"accepted_answer_id"` CreateTime int64 `json:"create_time"` @@ -107,11 +310,21 @@ type AdminQuestionInfo struct { UserInfo *UserBasicInfo `json:"user_info"` } +type OperationLevel string + +const ( + OperationLevelInfo OperationLevel = "info" + OperationLevelDanger OperationLevel = "danger" + OperationLevelWarning OperationLevel = "warning" + OperationLevelSecondary OperationLevel = "secondary" +) + type Operation struct { - Operation_Type string `json:"operation_type"` - Operation_Description string `json:"operation_description"` - Operation_Msg string `json:"operation_msg"` - Operation_Time int64 `json:"operation_time"` + Type string `json:"type"` + Description string `json:"description"` + Msg string `json:"msg"` + Time int64 `json:"time"` + Level OperationLevel `json:"level"` } type GetCloseTypeResp struct { @@ -132,47 +345,203 @@ type GetCloseTypeResp struct { type UserAnswerInfo struct { AnswerID string `json:"answer_id"` QuestionID string `json:"question_id"` - Adopted int `json:"adopted"` + Accepted int `json:"accepted"` VoteCount int `json:"vote_count"` CreateTime int `json:"create_time"` UpdateTime int `json:"update_time"` QuestionInfo struct { - Title string `json:"title"` - Tags []interface{} `json:"tags"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Tags []interface{} `json:"tags"` } `json:"question_info"` } type UserQuestionInfo struct { ID string `json:"question_id"` Title string `json:"title"` + UrlTitle string `json:"url_title"` VoteCount int `json:"vote_count"` Tags []interface{} `json:"tags"` ViewCount int `json:"view_count"` AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` - CreateTime int `json:"create_time"` - AcceptedAnswerId string `json:"accepted_answer_id"` + CreatedAt int64 `json:"created_at"` + AcceptedAnswerID string `json:"accepted_answer_id"` Status string `json:"status"` } -type QuestionSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Order string `json:"order" form:"order"` //Search order by - Tags []string `json:"tags" form:"tags"` //Search tag - TagIDs []string `json:"-" form:"-"` //Search tag - UserName string `json:"username" form:"username"` //Search username - UserID string `json:"-" form:"-"` +const ( + QuestionOrderCondNewest = "newest" + QuestionOrderCondActive = "active" + QuestionOrderCondHot = "hot" + QuestionOrderCondScore = "score" + QuestionOrderCondUnanswered = "unanswered" + QuestionOrderCondRecommend = "recommend" + QuestionOrderCondFrequent = "frequent" + + // HotInDays limit max days of the hottest question + HotInDays = 90 +) + +// QuestionPageReq query questions page +type QuestionPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"` + Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"` + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` + InDays int `validate:"omitempty,min=1" form:"in_days"` + + LoginUserID string `json:"-"` + UserIDBeSearched string `json:"-"` + TagID string `json:"-"` + ShowPending bool `json:"-"` +} + +const ( + QuestionPageRespOperationTypeAsked = "asked" + QuestionPageRespOperationTypeAnswered = "answered" + QuestionPageRespOperationTypeModified = "modified" +) + +type QuestionPageResp struct { + ID string `json:"id" ` + CreatedAt int64 `json:"created_at"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Description string `json:"description"` + Pin int `json:"pin"` // 1: unpin, 2: pin + Show int `json:"show"` // 0: show, 1: hide + Status int `json:"status"` + Tags []*TagResp `json:"tags"` + + // question statistical information + ViewCount int `json:"view_count"` + UniqueViewCount int `json:"unique_view_count"` + VoteCount int `json:"vote_count"` + AnswerCount int `json:"answer_count"` + CollectionCount int `json:"collection_count"` + FollowCount int `json:"follow_count"` + + // answer information + AcceptedAnswerID string `json:"accepted_answer_id"` + LastAnswerID string `json:"last_answer_id"` + LastAnsweredUserID string `json:"-"` + LastAnsweredAt time.Time `json:"-"` + + // operator information + OperatedAt int64 `json:"operated_at"` + Operator *QuestionPageRespOperator `json:"operator"` + OperationType string `json:"operation_type"` +} + +type QuestionPageRespOperator struct { + ID string `json:"id"` + Username string `json:"username"` + Rank int `json:"rank"` + DisplayName string `json:"display_name"` + Status string `json:"status"` + Avatar string `json:"avatar"` +} + +type AdminQuestionPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + StatusCond string `validate:"omitempty,oneof=normal closed deleted pending" form:"status"` + Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` + Status int `json:"-"` + LoginUserID string `json:"-"` } -type CmsQuestionSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 UserDeleted +func (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, err error) { + status, ok := entity.AdminQuestionSearchStatus[req.StatusCond] + if ok { + req.Status = status + } + if req.Status == 0 { + req.Status = 1 + } + return nil, nil +} + +// AdminAnswerPageReq admin answer page req +type AdminAnswerPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + StatusCond string `validate:"omitempty,oneof=normal deleted pending" form:"status"` + Query string `validate:"omitempty,gt=0,lte=100" form:"query"` + QuestionID string `validate:"omitempty,gt=0,lte=24" form:"question_id"` + QuestionTitle string `json:"-"` + AnswerID string `json:"-"` + Status int `json:"-"` + LoginUserID string `json:"-"` +} + +func (req *AdminAnswerPageReq) Check() (errField []*validator.FormErrorField, err error) { + req.QuestionID = uid.DeShortID(req.QuestionID) + if req.QuestionID == "0" { + req.QuestionID = "" + } + + if status, ok := entity.AdminAnswerSearchStatus[req.StatusCond]; ok { + req.Status = status + } + if req.Status == 0 { + req.Status = 1 + } + + // parse query condition + if len(req.Query) > 0 { + prefix := "answer:" + if strings.Contains(req.Query, prefix) { + req.AnswerID = uid.DeShortID(strings.TrimSpace(strings.TrimPrefix(req.Query, prefix))) + } else { + req.QuestionTitle = strings.TrimSpace(req.Query) + } + } + return nil, nil +} + +type AdminUpdateQuestionStatusReq struct { + QuestionID string `validate:"required" json:"question_id"` + Status string `validate:"required,oneof=available closed deleted" json:"status"` + UserID string `json:"-"` +} + +type PersonalQuestionPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` + LoginUserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +type PersonalAnswerPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` + LoginUserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +type PersonalCollectionPageReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` + UserID string `json:"-"` +} + +type GetQuestionLinkReq struct { + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1,max=100" form:"page_size"` + QuestionID string `validate:"required" form:"question_id"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"` + InDays int `validate:"omitempty,min=1" form:"in_days"` + + LoginUserID string `json:"-"` } -type AdminSetQuestionStatusRequest struct { - StatusStr string `json:"status" form:"status"` - QuestionID string `json:"question_id" form:"question_id"` +type GetQuestionLinkResp struct { + QuestionPageResp } diff --git a/internal/schema/rank_schema.go b/internal/schema/rank_schema.go index 81505c828..f83ccd6ed 100644 --- a/internal/schema/rank_schema.go +++ b/internal/schema/rank_schema.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema // GetRankPersonalWithPageReq get rank list page request @@ -12,8 +31,8 @@ type GetRankPersonalWithPageReq struct { UserID string `json:"-"` } -// GetRankPersonalWithPageResp rank response -type GetRankPersonalWithPageResp struct { +// GetRankPersonalPageResp rank response +type GetRankPersonalPageResp struct { // create time CreatedAt int64 `json:"created_at"` // object id @@ -26,6 +45,8 @@ type GetRankPersonalWithPageResp struct { ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` + // url title + UrlTitle string `json:"url_title"` // content Content string `json:"content"` // reputation diff --git a/internal/schema/reason_schema.go b/internal/schema/reason_schema.go index a6a833b2a..65b352bce 100644 --- a/internal/schema/reason_schema.go +++ b/internal/schema/reason_schema.go @@ -1,6 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "github.com/apache/answer/internal/base/translator" + "github.com/segmentfault/pacman/i18n" +) + type ReasonItem struct { + ReasonKey string `json:"reason_key"` ReasonType int `json:"reason_type"` Name string `json:"name"` Description string `json:"description"` @@ -14,3 +39,25 @@ type ReasonReq struct { // Action Action string `validate:"required" form:"action" json:"action"` } + +func (r *ReasonItem) Translate(keyPrefix string, lang i18n.Language) { + trField := func(fieldName, fieldData string) string { + // If fieldData is empty, means no need to translate + if len(fieldData) == 0 { + return fieldData + } + key := keyPrefix + "." + fieldName + fieldTr := translator.Tr(lang, key) + if fieldTr != key { + // If i18n key exists, return i18n value + return fieldTr + } + // If i18n key not exists, return fieldData original value + return fieldData + } + + r.ReasonKey = keyPrefix + r.Name = trField("name", r.Name) + r.Description = trField("desc", r.Description) + r.Placeholder = trField("placeholder", r.Placeholder) +} diff --git a/internal/schema/render_schema.go b/internal/schema/render_schema.go new file mode 100644 index 000000000..a66156f71 --- /dev/null +++ b/internal/schema/render_schema.go @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +// PostRenderReq post render request +type PostRenderReq struct { + Content string `json:"content"` +} diff --git a/internal/schema/report_schema.go b/internal/schema/report_schema.go index b4534f820..1f702df47 100644 --- a/internal/schema/report_schema.go +++ b/internal/schema/report_schema.go @@ -1,10 +1,23 @@ -package schema - -import ( - "time" +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ - "github.com/answerdev/answer/internal/base/constant" -) +package schema // AddReportReq add report request type AddReportReq struct { @@ -15,7 +28,9 @@ type AddReportReq struct { // report content Content string `validate:"omitempty,gt=0,lte=500" json:"content"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + CaptchaID string `json:"captcha_id"` // captcha_id + CaptchaCode string `json:"captcha_code"` } // GetReportListReq get report list all request @@ -49,52 +64,52 @@ type ReportHandleReq struct { // GetReportListPageDTO report list data transfer object type GetReportListPageDTO struct { - ObjectType string - Status string - Page int - PageSize int + Page int + PageSize int + Status int } // GetReportListPageResp get report list type GetReportListPageResp struct { - ID string `json:"id"` - ReportedUser *UserBasicInfo `json:"reported_user"` - ReportUser *UserBasicInfo `json:"report_user"` - - Content string `json:"content"` - FlaggedContent string `json:"flagged_content"` - OType string `json:"object_type"` - - ObjectID string `json:"-"` - QuestionID string `json:"question_id"` - AnswerID string `json:"answer_id"` - CommentID string `json:"comment_id"` - - Title string `json:"title"` - Excerpt string `json:"excerpt"` - - // create time - CreatedAt time.Time `json:"-"` - CreatedAtParsed int64 `json:"created_at"` - - UpdatedAt time.Time `json:"_"` - UpdatedAtParsed int64 `json:"updated_at"` - - Reason *ReasonItem `json:"reason"` - FlaggedReason *ReasonItem `json:"flagged_reason"` - - UserID string `json:"-"` - ReportedUserID string `json:"-"` - Status int `json:"-"` - ObjectType int `json:"-"` - ReportType int `json:"-"` - FlaggedType int `json:"-"` + FlagID string `json:"flag_id"` + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type" enums:"question,answer,comment"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + OriginalText string `json:"original_text"` + ParsedText string `json:"parsed_text"` + AnswerCount int `json:"answer_count"` + AnswerAccepted bool `json:"answer_accepted"` + Tags []*TagResp `json:"tags"` + ObjectStatus int `json:"object_status"` + ObjectShowStatus int `json:"object_show_status"` + AuthorUserInfo UserBasicInfo `json:"author_user_info"` + SubmitAt int64 `json:"submit_at"` + SubmitterUser UserBasicInfo `json:"submitter_user"` + Reason *ReasonItem `json:"reason"` + ReasonContent string `json:"reason_content"` } -// Format format result -func (r *GetReportListPageResp) Format() { - r.OType = constant.ObjectTypeNumberMapping[r.ObjectType] +// GetUnreviewedReportPostPageReq get unreviewed report post page request +type GetUnreviewedReportPostPageReq struct { + Page int `json:"page" form:"page"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} - r.CreatedAtParsed = r.CreatedAt.Unix() - r.UpdatedAtParsed = r.UpdatedAt.Unix() +// ReviewReportReq review report request +type ReviewReportReq struct { + FlagID string `validate:"required" json:"flag_id"` + OperationType string `validate:"required,oneof=edit_post close_post delete_post unlist_post ignore_report" json:"operation_type"` + CloseType int `validate:"omitempty" json:"close_type"` + CloseMsg string `validate:"omitempty" json:"close_msg"` + Title string `validate:"omitempty,notblank,gte=6,lte=150" json:"title"` + Content string `validate:"omitempty,notblank,gte=6,lte=65535" json:"content"` + Tags []*TagItem `validate:"omitempty,dive" json:"tags"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` } diff --git a/internal/schema/review_schema.go b/internal/schema/review_schema.go new file mode 100644 index 000000000..a4006d290 --- /dev/null +++ b/internal/schema/review_schema.go @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/pkg/uid" +) + +// UpdateReviewReq update review request +type UpdateReviewReq struct { + ReviewID int `validate:"required" json:"review_id"` + Status string `validate:"required,oneof=approve reject" json:"status"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +func (r *UpdateReviewReq) IsApprove() bool { + return r.Status == "approve" +} + +func (r *UpdateReviewReq) IsReject() bool { + return r.Status == "reject" +} + +// GetUnreviewedPostPageReq get review page request +type GetUnreviewedPostPageReq struct { + ObjectID string `validate:"omitempty" form:"object_id"` + Page int `validate:"omitempty" form:"page"` + ReviewerMapping map[string]string `json:"-"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +func (r *GetUnreviewedPostPageReq) Check() (errField []*validator.FormErrorField, err error) { + if len(r.ObjectID) > 0 { + r.Page = 1 + r.ObjectID = uid.DeShortID(r.ObjectID) + } + return +} + +// GetUnreviewedPostPageResp get review page response +type GetUnreviewedPostPageResp struct { + ReviewID int `json:"review_id"` + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type" enums:"question,answer,comment"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + OriginalText string `json:"original_text"` + ParsedText string `json:"parsed_text"` + Tags []*TagResp `json:"tags"` + ObjectStatus int `json:"object_status"` + ObjectShowStatus int `json:"object_show_status"` + AuthorUserInfo UserBasicInfo `json:"author_user_info"` + SubmitAt int64 `json:"submit_at"` + SubmitterDisplayName string `json:"submitter_display_name"` + Reason string `json:"reason"` +} diff --git a/internal/schema/revision_schema.go b/internal/schema/revision_schema.go index 826b30302..946d11653 100644 --- a/internal/schema/revision_schema.go +++ b/internal/schema/revision_schema.go @@ -1,7 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema import ( "time" + + "github.com/apache/answer/internal/base/constant" ) // AddRevisionDTO add revision request @@ -16,6 +37,8 @@ type AddRevisionDTO struct { Content string // log Log string + // status + Status int } // GetRevisionListReq get revision list all request @@ -24,27 +47,90 @@ type GetRevisionListReq struct { ObjectID string `validate:"required" comment:"object_id" form:"object_id"` } +const RevisionAuditApprove = "approve" +const RevisionAuditReject = "reject" + +type RevisionAuditReq struct { + // object id + ID string `validate:"required" comment:"id" form:"id"` + Operation string `validate:"required" comment:"operation" form:"operation"` //approve or reject + UserID string `json:"-"` + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` +} + +type RevisionSearch struct { + Page int `json:"page" form:"page"` // Query number of pages + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + UserID string `json:"-"` +} + +func (r RevisionSearch) GetCanReviewObjectTypes() []int { + objectType := make([]int, 0) + if r.CanReviewAnswer { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) + } + if r.CanReviewQuestion { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) + } + if r.CanReviewTag { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) + } + return objectType +} + +type GetUnreviewedRevisionResp struct { + Type string `json:"type"` + Info *UnreviewedRevisionInfoInfo `json:"info"` + UnreviewedInfo *GetRevisionResp `json:"unreviewed_info"` +} + // GetRevisionResp get revision response type GetRevisionResp struct { - // id - ID string `json:"id"` - // user id - UserID string `json:"use_id"` - // object id - ObjectID string `json:"object_id"` - // object type - ObjectType int `json:"-"` - // title - Title string `json:"title"` - // content - Content string `json:"-"` - // content parsed - ContentParsed interface{} `json:"content"` - // revision status(normal: 1; delete 2) - Status int `json:"status"` - // create time + ID string `json:"id"` + UserID string `json:"use_id"` + ObjectID string `json:"object_id"` + ObjectType int `json:"-"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Content string `json:"-"` + ContentParsed interface{} `json:"content"` + Status int `json:"status"` CreatedAt time.Time `json:"-"` CreatedAtParsed int64 `json:"create_at"` UserInfo UserBasicInfo `json:"user_info"` Log string `json:"reason"` } + +// GetReviewingTypeReq get reviewing type request +type GetReviewingTypeReq struct { + CanReviewQuestion bool `json:"-"` + CanReviewAnswer bool `json:"-"` + CanReviewTag bool `json:"-"` + IsAdmin bool `json:"-"` + UserID string `json:"-"` +} + +func (r *GetReviewingTypeReq) GetCanReviewObjectTypes() []int { + objectType := make([]int, 0) + if r.CanReviewAnswer { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.AnswerObjectType]) + } + if r.CanReviewQuestion { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.QuestionObjectType]) + } + if r.CanReviewTag { + objectType = append(objectType, constant.ObjectTypeStrMapping[constant.TagObjectType]) + } + return objectType +} + +// GetReviewingTypeResp get reviewing type response +type GetReviewingTypeResp struct { + Name string `json:"name"` + Label string `json:"label"` + TodoAmount int64 `json:"todo_amount"` +} diff --git a/internal/schema/role_schema.go b/internal/schema/role_schema.go new file mode 100644 index 000000000..728f3f336 --- /dev/null +++ b/internal/schema/role_schema.go @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +// GetRoleResp get role response +type GetRoleResp struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 5cf8c5776..8667c74a5 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -1,47 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "regexp" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/plugin" +) + type SearchDTO struct { - UserID string // UserID current login user ID - Query string `validate:"required,gte=1,lte=60" json:"q" form:"q"` // Query the query string - Page int `validate:"omitempty,min=1" form:"page,default=1" json:"page"` //Query number of pages - Size int `validate:"omitempty,min=1,max=50" form:"size,default=30" json:"size"` //Search page size - Order string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" json:"order" enums:"newest,active,score,relevance"` + Query string `validate:"required,gte=1,lte=60" form:"q"` + Page int `validate:"omitempty,min=1" form:"page,default=1"` + Size int `validate:"omitempty,min=1,max=50" form:"size,default=30"` + Order string `validate:"required,oneof=newest active score relevance" form:"order,default=relevance" enums:"newest,active,score,relevance"` + CaptchaID string `form:"captcha_id"` + CaptchaCode string `form:"captcha_code"` + UserID string `json:"-"` +} + +func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) { + // Replace special characters. + // Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format. + replacedContent, patterns := ReplaceSearchContent(s.Query) + s.Query = strings.Join(append(patterns, replacedContent), " ") + + return nil, nil +} + +func ReplaceSearchContent(content string) (string, []string) { + // Define the regular expressions for key:value pairs and [tag] + keyValueRegex := regexp.MustCompile(`\w+:\S+`) + tagRegex := regexp.MustCompile(`\[\w+\]`) + // Define the pattern for characters to replace + replaceCharsPattern := regexp.MustCompile(`[+#.<>\-_()*]`) + + // Extract key:value pairs + keyValues := keyValueRegex.FindAllString(content, -1) + // Extract [tag] + tags := tagRegex.FindAllString(content, -1) + + // Replace key:value pairs and [tag] with empty string + contentWithoutPatterns := keyValueRegex.ReplaceAllString(content, "") + contentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, "") + + // Replace characters with pattern [+#.<>_()*] with space + replacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, " ") + + return strings.TrimSpace(replacedContent), append(keyValues, tags...) +} + +type SearchCondition struct { + // search target type: all/question/answer + TargetType string + // search query user id + UserID string + // vote amount + VoteAmount int + // only show not accepted answer's question + NotAccepted bool + // view amount + Views int + // answer count + AnswerAmount int + // only show accepted answer + Accepted bool + // only show this question's answer + QuestionID string + // search query tags + Tags [][]string + // search query keywords + Words []string +} + +// SearchAll check if search all +func (s *SearchCondition) SearchAll() bool { + return len(s.TargetType) == 0 +} + +// SearchQuestion check if search only need question +func (s *SearchCondition) SearchQuestion() bool { + return s.TargetType == constant.QuestionObjectType +} + +// SearchAnswer check if search only need answer +func (s *SearchCondition) SearchAnswer() bool { + return s.TargetType == constant.AnswerObjectType +} + +// Convert2PluginSearchCond convert to plugin search condition +func (s *SearchCondition) Convert2PluginSearchCond(page, pageSize int, order string) *plugin.SearchBasicCond { + basic := &plugin.SearchBasicCond{ + Page: page, + PageSize: pageSize, + Words: s.Words, + TagIDs: s.Tags, + UserID: s.UserID, + Order: plugin.SearchOrderCond(order), + QuestionID: s.QuestionID, + VoteAmount: s.VoteAmount, + ViewAmount: s.Views, + AnswerAmount: s.AnswerAmount, + } + if s.Accepted { + basic.AnswerAccepted = plugin.AcceptedCondTrue + } else { + basic.AnswerAccepted = plugin.AcceptedCondAll + } + if s.NotAccepted { + basic.QuestionAccepted = plugin.AcceptedCondFalse + } else { + basic.QuestionAccepted = plugin.AcceptedCondAll + } + return basic } type SearchObject struct { ID string `json:"id"` + QuestionID string `json:"question_id"` Title string `json:"title"` + UrlTitle string `json:"url_title"` Excerpt string `json:"excerpt"` CreatedAtParsed int64 `json:"created_at"` VoteCount int `json:"vote_count"` Accepted bool `json:"accepted"` AnswerCount int `json:"answer_count"` // user info - UserInfo *UserBasicInfo `json:"user_info"` + UserInfo *SearchObjectUser `json:"user_info"` // tags - Tags []TagResp `json:"tags"` + Tags []*TagResp `json:"tags"` // Status StatusStr string `json:"status"` } +type SearchObjectUser struct { + ID string `json:"id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Rank int `json:"rank"` + Status string `json:"status"` +} + type TagResp struct { + ID string `json:"-"` SlugName string `json:"slug_name"` DisplayName string `json:"display_name"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` } -type SearchResp struct { +type SearchResult struct { // object_type ObjectType string `json:"object_type"` // this object - Object SearchObject `json:"object"` + Object *SearchObject `json:"object"` } -type SearchListResp struct { +type SearchResp struct { Total int64 `json:"count"` // search response - SearchResp []SearchResp `json:"list"` - // extra fields - Extra interface{} `json:"extra"` + SearchResults []*SearchResult `json:"list"` +} + +type SearchDescResp struct { + Name string `json:"name"` + Icon string `json:"icon"` + Link string `json:"link"` } diff --git a/internal/schema/search_schema_test.go b/internal/schema/search_schema_test.go new file mode 100644 index 000000000..fa8305342 --- /dev/null +++ b/internal/schema/search_schema_test.go @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplaceSearchContent(t *testing.T) { + content := "user:aaa [tag] ssssfdfdf-as#fsadf" + replacedContent, patterns := ReplaceSearchContent(content) + ret := strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa [tag] ssssfdfdf as fsadf", ret) + + content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3" + replacedContent, patterns = ReplaceSearchContent(content) + ret = strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf", ret) +} diff --git a/internal/schema/simple_obj_info_schema.go b/internal/schema/simple_obj_info_schema.go index 312772dbd..a9bcf3b19 100644 --- a/internal/schema/simple_obj_info_schema.go +++ b/internal/schema/simple_obj_info_schema.go @@ -1,14 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" +) + // SimpleObjectInfo simple object info type SimpleObjectInfo struct { - ObjectID string `json:"object_id"` - ObjectCreator string `json:"object_creator"` - QuestionID string `json:"question_id"` - AnswerID string `json:"answer_id"` - CommentID string `json:"comment_id"` - TagID string `json:"tag_id"` - ObjectType string `json:"object_type"` - Title string `json:"title"` - Content string `json:"content"` + ObjectID string `json:"object_id"` + ObjectCreatorUserID string `json:"object_creator_user_id"` + QuestionID string `json:"question_id"` + QuestionStatus int `json:"question_status"` + AnswerID string `json:"answer_id"` + AnswerStatus int `json:"answer_status"` + CommentID string `json:"comment_id"` + CommentStatus int `json:"comment_status"` + TagID string `json:"tag_id"` + ObjectType string `json:"object_type"` + Title string `json:"title"` + Content string `json:"content"` +} + +// IsDeleted is deleted +func (s *SimpleObjectInfo) IsDeleted() bool { + switch s.ObjectType { + case constant.QuestionObjectType: + return s.QuestionStatus == entity.QuestionStatusDeleted + case constant.AnswerObjectType: + return s.AnswerStatus == entity.AnswerStatusDeleted + case constant.CommentObjectType: + return s.CommentStatus == entity.CommentStatusDeleted + } + return false +} + +type UnreviewedRevisionInfoInfo struct { + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type"` + ObjectCreatorUserID string `json:"object_creator_user_id"` + Title string `json:"title"` + UrlTitle string `json:"url_title"` + Content string `json:"content"` + Html string `json:"html"` + AnswerCount int `json:"answer_count"` + AnswerAccepted bool `json:"answer_accepted"` + Tags []*TagResp `json:"tags"` + Status int `json:"status"` + ShowStatus int `json:"show_status"` } diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index ff93e72d7..19f01ffdb 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -1,17 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema +import ( + "context" + "fmt" + "net/mail" + "net/url" + "path/filepath" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/segmentfault/pacman/errors" +) + // SiteGeneralReq site general request type SiteGeneralReq struct { - Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` + Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"` + ShortDescription string `validate:"omitempty,sanitizer,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"` + SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` + CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"` +} + +func (r *SiteGeneralReq) FormatSiteUrl() { + parsedUrl, err := url.Parse(r.SiteUrl) + if err != nil { + return + } + r.SiteUrl = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host) + if len(parsedUrl.Path) > 0 { + r.SiteUrl = r.SiteUrl + parsedUrl.Path + r.SiteUrl = strings.TrimSuffix(r.SiteUrl, "/") + } } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { - Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` - Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` - Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` + Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"` + TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` + DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"` + GravatarBaseURL string `validate:"omitempty" json:"gravatar_base_url"` +} + +// SiteBrandingReq site branding request +type SiteBrandingReq struct { + Logo string `validate:"omitempty,gt=0,lte=512" form:"logo" json:"logo"` + MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"` + SquareIcon string `validate:"omitempty,gt=0,lte=512" form:"square_icon" json:"square_icon"` + Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"` +} + +// SiteWriteReq site write request +type SiteWriteReq struct { + RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"` + RequiredTag bool `validate:"omitempty" json:"required_tag"` + RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"` + ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"` + MaxImageSize int `validate:"omitempty,gt=0" json:"max_image_size"` + MaxAttachmentSize int `validate:"omitempty,gt=0" json:"max_attachment_size"` + MaxImageMegapixel int `validate:"omitempty,gt=0" json:"max_image_megapixel"` + AuthorizedImageExtensions []string `validate:"omitempty" json:"authorized_image_extensions"` + AuthorizedAttachmentExtensions []string `validate:"omitempty" json:"authorized_attachment_extensions"` + UserID string `json:"-"` +} + +func (s *SiteWriteResp) GetMaxImageSize() int64 { + if s.MaxImageSize <= 0 { + return constant.DefaultMaxImageSize + } + return int64(s.MaxImageSize) * 1024 * 1024 +} + +func (s *SiteWriteResp) GetMaxAttachmentSize() int64 { + if s.MaxAttachmentSize <= 0 { + return constant.DefaultMaxAttachmentSize + } + return int64(s.MaxAttachmentSize) * 1024 * 1024 +} + +func (s *SiteWriteResp) GetMaxImageMegapixel() int { + if s.MaxImageMegapixel <= 0 { + return constant.DefaultMaxImageMegapixel + } + return s.MaxImageMegapixel * 1000 * 1000 +} + +// SiteWriteTag site write response tag +type SiteWriteTag struct { + SlugName string `validate:"required" json:"slug_name"` + DisplayName string `json:"display_name"` +} + +// SiteLegalReq site branding request +type SiteLegalReq struct { + TermsOfServiceOriginalText string `json:"terms_of_service_original_text"` + TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"` + PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"` + PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"` + ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` +} + +// GetSiteLegalInfoReq site site legal request +type GetSiteLegalInfoReq struct { + InfoType string `validate:"required,oneof=tos privacy" form:"info_type"` +} + +func (r *GetSiteLegalInfoReq) IsTOS() bool { + return r.InfoType == "tos" +} + +func (r *GetSiteLegalInfoReq) IsPrivacy() bool { + return r.InfoType == "privacy" +} + +// GetSiteLegalInfoResp get site legal info response +type GetSiteLegalInfoResp struct { + TermsOfServiceOriginalText string `json:"terms_of_service_original_text,omitempty"` + TermsOfServiceParsedText string `json:"terms_of_service_parsed_text,omitempty"` + PrivacyPolicyOriginalText string `json:"privacy_policy_original_text,omitempty"` + PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"` +} + +// SiteUsersReq site users config request +type SiteUsersReq struct { + DefaultAvatar string `validate:"required,oneof=system gravatar" json:"default_avatar"` + GravatarBaseURL string `json:"gravatar_base_url"` + AllowUpdateDisplayName bool `json:"allow_update_display_name"` + AllowUpdateUsername bool `json:"allow_update_username"` + AllowUpdateAvatar bool `json:"allow_update_avatar"` + AllowUpdateBio bool `json:"allow_update_bio"` + AllowUpdateWebsite bool `json:"allow_update_website"` + AllowUpdateLocation bool `json:"allow_update_location"` +} + +// SiteLoginReq site login request +type SiteLoginReq struct { + AllowNewRegistrations bool `json:"allow_new_registrations"` + AllowEmailRegistrations bool `json:"allow_email_registrations"` + AllowPasswordLogin bool `json:"allow_password_login"` + LoginRequired bool `json:"login_required"` + AllowEmailDomains []string `json:"allow_email_domains"` +} + +// SiteCustomCssHTMLReq site custom css html +type SiteCustomCssHTMLReq struct { + CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"` + CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"` + CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"` + CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"` + CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"` +} + +// SiteThemeReq site theme config +type SiteThemeReq struct { + Theme string `validate:"required,gt=0,lte=255" json:"theme"` + ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"` + ColorScheme string `validate:"omitempty,gt=0,lte=100" json:"color_scheme"` +} + +type SiteSeoReq struct { + Permalink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"` + Robots string `validate:"required" form:"robots" json:"robots"` +} + +func (s *SiteSeoResp) IsShortLink() bool { + return s.Permalink == constant.PermalinkQuestionIDAndTitleByShortID || + s.Permalink == constant.PermalinkQuestionIDByShortID } // SiteGeneralResp site general response @@ -20,9 +197,84 @@ type SiteGeneralResp SiteGeneralReq // SiteInterfaceResp site interface response type SiteInterfaceResp SiteInterfaceReq +// SiteBrandingResp site branding response +type SiteBrandingResp SiteBrandingReq + +// SiteLoginResp site login response +type SiteLoginResp SiteLoginReq + +// SiteCustomCssHTMLResp site custom css html response +type SiteCustomCssHTMLResp SiteCustomCssHTMLReq + +// SiteUsersResp site users response +type SiteUsersResp SiteUsersReq + +// SiteThemeResp site theme response +type SiteThemeResp struct { + ThemeOptions []*ThemeOption `json:"theme_options"` + Theme string `json:"theme"` + ThemeConfig map[string]interface{} `json:"theme_config"` + ColorScheme string `json:"color_scheme"` +} + +func (s *SiteThemeResp) TrTheme(ctx context.Context) { + la := handler.GetLangByCtx(ctx) + for _, option := range s.ThemeOptions { + tr := translator.Tr(la, option.Value) + // if tr is equal the option value means not found translation, so use the original label + if tr != option.Value { + option.Label = tr + } + } +} + +// ThemeOption get label option +type ThemeOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// SiteWriteResp site write response +type SiteWriteResp SiteWriteReq + +// SiteLegalResp site write response +type SiteLegalResp SiteLegalReq + +// SiteLegalSimpleResp site write response +type SiteLegalSimpleResp struct { + ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"` +} + +// SiteSeoResp site write response +type SiteSeoResp SiteSeoReq + +// SiteInfoResp get site info response type SiteInfoResp struct { - General *SiteGeneralResp `json:"general"` - Face *SiteInterfaceResp `json:"interface"` + General *SiteGeneralResp `json:"general"` + Interface *SiteInterfaceResp `json:"interface"` + Branding *SiteBrandingResp `json:"branding"` + Login *SiteLoginResp `json:"login"` + Theme *SiteThemeResp `json:"theme"` + CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` + SiteSeo *SiteSeoResp `json:"site_seo"` + SiteUsers *SiteUsersResp `json:"site_users"` + Write *SiteWriteResp `json:"site_write"` + Legal *SiteLegalSimpleResp `json:"site_legal"` + Version string `json:"version"` + Revision string `json:"revision"` +} +type TemplateSiteInfoResp struct { + General *SiteGeneralResp `json:"general"` + Interface *SiteInterfaceResp `json:"interface"` + Branding *SiteBrandingResp `json:"branding"` + SiteSeo *SiteSeoResp `json:"site_seo"` + CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` + Title string + Year string + Canonical string + JsonLD string + Keywords string + Description string } // UpdateSMTPConfigReq get smtp config request @@ -31,21 +283,188 @@ type UpdateSMTPConfigReq struct { FromName string `validate:"omitempty,gt=0,lte=256" json:"from_name"` SMTPHost string `validate:"omitempty,gt=0,lte=256" json:"smtp_host"` SMTPPort int `validate:"omitempty,min=1,max=65535" json:"smtp_port"` - Encryption string `validate:"omitempty,oneof=SSL" json:"encryption"` // "" SSL + Encryption string `validate:"omitempty,oneof=SSL TLS" json:"encryption"` // "" SSL TLS SMTPUsername string `validate:"omitempty,gt=0,lte=256" json:"smtp_username"` SMTPPassword string `validate:"omitempty,gt=0,lte=256" json:"smtp_password"` SMTPAuthentication bool `validate:"omitempty" json:"smtp_authentication"` TestEmailRecipient string `validate:"omitempty,email" json:"test_email_recipient"` } +func (r *UpdateSMTPConfigReq) Check() (errField []*validator.FormErrorField, err error) { + _, err = mail.ParseAddress(r.FromName) + if err == nil { + return append(errField, &validator.FormErrorField{ + ErrorField: "from_name", + ErrorMsg: reason.SMTPConfigFromNameCannotBeEmail, + }), errors.BadRequest(reason.SMTPConfigFromNameCannotBeEmail) + } + return nil, nil +} + // GetSMTPConfigResp get smtp config response type GetSMTPConfigResp struct { FromEmail string `json:"from_email"` FromName string `json:"from_name"` SMTPHost string `json:"smtp_host"` SMTPPort int `json:"smtp_port"` - Encryption string `json:"encryption"` // "" SSL + Encryption string `json:"encryption"` // "" SSL TLS SMTPUsername string `json:"smtp_username"` SMTPPassword string `json:"smtp_password"` SMTPAuthentication bool `json:"smtp_authentication"` } + +// GetManifestJsonResp get manifest json response +type GetManifestJsonResp struct { + ManifestVersion int `json:"manifest_version"` + Version string `json:"version"` + Revision string `json:"revision"` + ShortName string `json:"short_name"` + Name string `json:"name"` + Icons []ManifestJsonIcon `json:"icons"` + StartUrl string `json:"start_url"` + Display string `json:"display"` + ThemeColor string `json:"theme_color"` + BackgroundColor string `json:"background_color"` +} + +type ManifestJsonIcon struct { + Src string `json:"src"` + Sizes string `json:"sizes"` + Type string `json:"type"` +} + +func CreateManifestJsonIcons(icon string) []ManifestJsonIcon { + ext := filepath.Ext(icon) + if ext == "" { + ext = "png" + } else { + ext = strings.ToLower(ext[1:]) + } + iconType := fmt.Sprintf("image/%s", ext) + return []ManifestJsonIcon{ + { + Src: icon, + Sizes: "16x16", + Type: iconType, + }, + { + Src: icon, + Sizes: "32x32", + Type: iconType, + }, + { + Src: icon, + Sizes: "48x48", + Type: iconType, + }, + { + Src: icon, + Sizes: "128x128", + Type: iconType, + }, + } +} + +const ( + // PrivilegeLevel1 low + PrivilegeLevel1 PrivilegeLevel = 1 + // PrivilegeLevel2 medium + PrivilegeLevel2 PrivilegeLevel = 2 + // PrivilegeLevel3 high + PrivilegeLevel3 PrivilegeLevel = 3 + // PrivilegeLevelCustom custom + PrivilegeLevelCustom PrivilegeLevel = 99 +) + +type PrivilegeLevel int +type PrivilegeOptions []*PrivilegeOption + +func (p PrivilegeOptions) Choose(level PrivilegeLevel) (option *PrivilegeOption) { + for _, op := range p { + if op.Level == level { + return op + } + } + return nil +} + +// GetPrivilegesConfigResp get privileges config response +type GetPrivilegesConfigResp struct { + Options []*PrivilegeOption `json:"options"` + SelectedLevel PrivilegeLevel `json:"selected_level"` +} + +// PrivilegeOption privilege option +type PrivilegeOption struct { + Level PrivilegeLevel `json:"level"` + LevelDesc string `json:"level_desc"` + Privileges []*constant.Privilege `validate:"dive" json:"privileges"` +} + +// UpdatePrivilegesConfigReq update privileges config request +type UpdatePrivilegesConfigReq struct { + Level PrivilegeLevel `validate:"required,min=1,max=3|eq=99" json:"level"` + CustomPrivileges []*constant.Privilege `validate:"dive" json:"custom_privileges"` +} + +var ( + DefaultPrivilegeOptions PrivilegeOptions + DefaultCustomPrivilegeOption *PrivilegeOption + privilegeOptionsLevelMapping = map[string][]int{ + constant.RankQuestionAddKey: {1, 1, 1}, + constant.RankAnswerAddKey: {1, 1, 1}, + constant.RankCommentAddKey: {1, 1, 1}, + constant.RankReportAddKey: {1, 1, 1}, + constant.RankCommentVoteUpKey: {1, 1, 1}, + constant.RankLinkUrlLimitKey: {1, 10, 10}, + constant.RankQuestionVoteUpKey: {1, 8, 15}, + constant.RankAnswerVoteUpKey: {1, 8, 15}, + constant.RankQuestionVoteDownKey: {125, 125, 125}, + constant.RankAnswerVoteDownKey: {125, 125, 125}, + constant.RankInviteSomeoneToAnswerKey: {1, 500, 1000}, + constant.RankTagAddKey: {1, 750, 1500}, + constant.RankTagEditKey: {1, 50, 100}, + constant.RankQuestionEditKey: {1, 100, 200}, + constant.RankAnswerEditKey: {1, 100, 200}, + constant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000}, + constant.RankAnswerEditWithoutReviewKey: {1, 1000, 2000}, + constant.RankQuestionAuditKey: {1, 1000, 2000}, + constant.RankAnswerAuditKey: {1, 1000, 2000}, + constant.RankTagAuditKey: {1, 2500, 5000}, + constant.RankTagEditWithoutReviewKey: {1, 10000, 20000}, + constant.RankTagSynonymKey: {1, 10000, 20000}, + } +) + +func init() { + DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{ + Level: PrivilegeLevel1, + LevelDesc: reason.PrivilegeLevel1Desc, + }, &PrivilegeOption{ + Level: PrivilegeLevel2, + LevelDesc: reason.PrivilegeLevel2Desc, + }, &PrivilegeOption{ + Level: PrivilegeLevel3, + LevelDesc: reason.PrivilegeLevel3Desc, + }) + + for _, option := range DefaultPrivilegeOptions { + for _, privilege := range constant.RankAllPrivileges { + if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 { + continue + } + option.Privileges = append(option.Privileges, &constant.Privilege{ + Label: privilege.Label, + Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1], + Key: privilege.Key, + }) + } + } + + // set up default custom privilege option + DefaultCustomPrivilegeOption = &PrivilegeOption{ + Level: PrivilegeLevelCustom, + LevelDesc: reason.PrivilegeLevelCustomDesc, + Privileges: DefaultPrivilegeOptions[0].Privileges, + } +} diff --git a/internal/schema/sitemap_schema.go b/internal/schema/sitemap_schema.go new file mode 100644 index 000000000..e29e41586 --- /dev/null +++ b/internal/schema/sitemap_schema.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +type SiteMapList struct { + QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"` + MaxPageNum []int `json:"max_page_num"` +} + +type SiteMapPageList struct { + PageData []*SiteMapQuestionInfo `json:"page_data"` +} + +type SiteMapQuestionInfo struct { + ID string `json:"id"` + Title string `json:"title"` + UpdateTime string `json:"time"` +} diff --git a/internal/schema/tag_list_schema.go b/internal/schema/tag_list_schema.go index e23d9114b..702f5e844 100644 --- a/internal/schema/tag_list_schema.go +++ b/internal/schema/tag_list_schema.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema // AddTagListReq add tag list request diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 7680c5db1..a51b2d2d5 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -1,17 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema import ( "strings" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/validator" - "github.com/segmentfault/pacman/errors" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/pkg/converter" ) // SearchTagLikeReq get tag list all request type SearchTagLikeReq struct { // tag - Tag string `validate:"required,gt=0,lte=35" form:"tag"` + Tag string `validate:"omitempty" form:"tag"` + IsAdmin bool `json:"-"` +} + +// SearchTagsBySlugName search tags by slug name +type SearchTagsBySlugName struct { + // slug name list split by ',' + Tags string `form:"tags"` } // GetTagInfoReq get tag info request @@ -19,51 +44,54 @@ type GetTagInfoReq struct { // tag id ID string `validate:"omitempty" form:"id"` // tag slug name - Name string `validate:"omitempty,gt=0,lte=35" form:"name"` + Name string `validate:"omitempty,gt=0,lte=35" form:"name"` + UserID string `json:"-"` + CanEdit bool `json:"-"` + CanDelete bool `json:"-"` + CanMerge bool `json:"-"` + CanRecover bool `json:"-"` +} + +type GetTamplateTagInfoReq struct { + // tag id + ID string `validate:"omitempty" form:"id"` + // tag slug name + Name string `validate:"omitempty" form:"name"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + Page int `validate:"omitempty,min=1" form:"page"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` } -func (r *GetTagInfoReq) Check() (errField *validator.ErrorField, err error) { - if len(r.ID) == 0 && len(r.Name) == 0 { - return nil, errors.BadRequest(reason.RequestFormatError) - } +func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) { r.Name = strings.ToLower(r.Name) return nil, nil } // GetTagResp get tag response type GetTagResp struct { - // tag id - TagID string `json:"tag_id"` - // created time - CreatedAt int64 `json:"created_at"` - // updated time - UpdatedAt int64 `json:"updated_at"` - // slug name - SlugName string `json:"slug_name"` - // display name - DisplayName string `json:"display_name"` - // excerpt - Excerpt string `json:"excerpt"` - // original text - OriginalText string `json:"original_text"` - // parsed text - ParsedText string `json:"parsed_text"` - // follower amount - FollowCount int `json:"follow_count"` - // question amount - QuestionCount int `json:"question_count"` - // is follower - IsFollower bool `json:"is_follower"` - // MemberActions + TagID string `json:"tag_id"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + SlugName string `json:"slug_name"` + DisplayName string `json:"display_name"` + Excerpt string `json:"excerpt"` + OriginalText string `json:"original_text"` + ParsedText string `json:"parsed_text"` + Description string `json:"description"` + FollowCount int `json:"follow_count"` + QuestionCount int `json:"question_count"` + IsFollower bool `json:"is_follower"` + Status string `json:"status"` MemberActions []*PermissionMemberAction `json:"member_actions"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` } func (tr *GetTagResp) GetExcerpt() { - excerpt := strings.TrimSpace(tr.OriginalText) + excerpt := strings.TrimSpace(tr.ParsedText) idx := strings.Index(excerpt, "\n") if idx >= 0 { excerpt = excerpt[0:idx] @@ -81,6 +109,8 @@ type GetTagPageResp struct { DisplayName string `json:"display_name"` // excerpt Excerpt string `json:"excerpt"` + //description + Description string `json:"description"` // original text OriginalText string `json:"original_text"` // parsed_text @@ -95,10 +125,12 @@ type GetTagPageResp struct { CreatedAt int64 `json:"created_at"` // updated time UpdatedAt int64 `json:"updated_at"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` } func (tr *GetTagPageResp) GetExcerpt() { - excerpt := strings.TrimSpace(tr.OriginalText) + excerpt := strings.TrimSpace(tr.ParsedText) idx := strings.Index(excerpt, "\n") if idx >= 0 { excerpt = excerpt[0:idx] @@ -107,7 +139,7 @@ func (tr *GetTagPageResp) GetExcerpt() { } type TagChange struct { - ObjectId string `json:"object_id"` // object_id + ObjectID string `json:"object_id"` // object_id Tags []*TagItem `json:"tags"` // tags name // user id UserID string `json:"-"` @@ -121,7 +153,7 @@ type TagItem struct { // original text OriginalText string `validate:"omitempty" json:"original_text"` // parsed text - ParsedText string `validate:"omitempty" json:"parsed_text"` + ParsedText string `json:"-"` } // RemoveTagReq delete tag request @@ -132,6 +164,31 @@ type RemoveTagReq struct { UserID string `json:"-"` } +// AddTagReq add tag request +type AddTagReq struct { + // slug_name + SlugName string `validate:"required,gt=0,lte=35" json:"slug_name"` + // display_name + DisplayName string `validate:"required,gt=0,lte=35" json:"display_name"` + // original text + OriginalText string `validate:"required,gt=0,lte=65536" json:"original_text"` + // parsed text + ParsedText string `json:"-"` + // user id + UserID string `json:"-"` +} + +func (req *AddTagReq) Check() (errFields []*validator.FormErrorField, err error) { + req.ParsedText = converter.Markdown2HTML(req.OriginalText) + req.SlugName = strings.ToLower(req.SlugName) + return nil, nil +} + +// AddTagResp add tag response +type AddTagResp struct { + SlugName string `json:"slug_name"` +} + // UpdateTagReq update tag request type UpdateTagReq struct { // tag_id @@ -143,20 +200,30 @@ type UpdateTagReq struct { // original text OriginalText string `validate:"omitempty" json:"original_text"` // parsed text - ParsedText string `validate:"omitempty" json:"parsed_text"` + ParsedText string `json:"-"` // edit summary EditSummary string `validate:"omitempty" json:"edit_summary"` // user id - UserID string `json:"-"` + UserID string `json:"-"` + NoNeedReview bool `json:"-"` } -func (r *UpdateTagReq) Check() (errField *validator.ErrorField, err error) { - if len(r.EditSummary) == 0 { - r.EditSummary = "tag.edit.summary" // to i18n - } +func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) { + r.ParsedText = converter.Markdown2HTML(r.OriginalText) return nil, nil } +// RecoverTagReq update tag request +type RecoverTagReq struct { + TagID string `validate:"required" json:"tag_id"` + UserID string `json:"-"` +} + +// UpdateTagResp update tag response +type UpdateTagResp struct { + WaitForReview bool `json:"wait_for_review"` +} + // GetTagWithPageReq get tag list page request type GetTagWithPageReq struct { // page @@ -177,10 +244,21 @@ type GetTagWithPageReq struct { type GetTagSynonymsReq struct { // tag_id TagID string `validate:"required" form:"tag_id"` + // user id + UserID string `json:"-"` + // whether user can edit it + CanEdit bool `json:"-"` } // GetTagSynonymsResp get tag synonyms response type GetTagSynonymsResp struct { + // synonyms + Synonyms []*TagSynonym `json:"synonyms"` + // MemberActions + MemberActions []*PermissionMemberAction `json:"member_actions"` +} + +type TagSynonym struct { // tag id TagID string `json:"tag_id"` // slug name @@ -217,4 +295,29 @@ type GetFollowingTagsResp struct { DisplayName string `json:"display_name"` // if main tag slug name is not empty, this tag is synonymous with the main tag MainTagSlugName string `json:"main_tag_slug_name"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` +} + +// GetTagBasicResp get tag basic response +type GetTagBasicResp struct { + TagID string `json:"tag_id"` + SlugName string `json:"slug_name"` + DisplayName string `json:"display_name"` + Recommend bool `json:"recommend"` + Reserved bool `json:"reserved"` +} + +// MergeTagReq merge tag request +type MergeTagReq struct { + // source tag id + SourceTagID string `validate:"required" json:"source_tag_id"` + // target tag id + TargetTagID string `validate:"required" json:"target_tag_id"` + // user id + UserID string `json:"-"` +} + +// MergeTagResp merge tag response +type MergeTagResp struct { } diff --git a/internal/schema/template_schema.go b/internal/schema/template_schema.go new file mode 100644 index 000000000..ae90aa945 --- /dev/null +++ b/internal/schema/template_schema.go @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "time" + +type Paginator struct { + Pages []int + Totalpages int + Prevpage int + Nextpage int + Currpage int +} + +type QAPageJsonLD struct { + Context string `json:"@context"` + Type string `json:"@type"` + MainEntity struct { + Type string `json:"@type"` + Name string `json:"name"` + Text string `json:"text"` + AnswerCount int `json:"answerCount"` + UpvoteCount int `json:"upvoteCount"` + DateCreated time.Time `json:"dateCreated"` + Author struct { + URL string `json:"url"` + Type string `json:"@type"` + Name string `json:"name"` + } `json:"author"` + AcceptedAnswer *AcceptedAnswerItem `json:"acceptedAnswer,omitempty"` + SuggestedAnswer []*SuggestedAnswerItem `json:"suggestedAnswer"` + } `json:"mainEntity"` +} + +type AcceptedAnswerItem struct { + Type string `json:"@type"` + Text string `json:"text"` + DateCreated time.Time `json:"dateCreated"` + UpvoteCount int `json:"upvoteCount"` + URL string `json:"url"` + Author struct { + URL string `json:"url"` + Type string `json:"@type"` + Name string `json:"name"` + } `json:"author"` +} + +type SuggestedAnswerItem struct { + Type string `json:"@type"` + Text string `json:"text"` + DateCreated time.Time `json:"dateCreated"` + UpvoteCount int `json:"upvoteCount"` + URL string `json:"url"` + Author struct { + URL string `json:"url"` + Type string `json:"@type"` + Name string `json:"name"` + } `json:"author"` +} diff --git a/internal/schema/theme_schema.go b/internal/schema/theme_schema.go index d5eb49b69..1d611575b 100644 --- a/internal/schema/theme_schema.go +++ b/internal/schema/theme_schema.go @@ -1,22 +1,27 @@ -package schema +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ -// GetThemeOption get label option -type GetThemeOption struct { - Label string `json:"label"` - Value string `json:"value"` -} +package schema -var GetThemeOptions = []*GetThemeOption{ +var GetThemeOptions = []*ThemeOption{ { Label: "Default", Value: "default", }, - { - Label: "Black", - Value: "black", - }, - { - Label: "White", - Value: "white", - }, } diff --git a/internal/schema/user_external_login_schema.go b/internal/schema/user_external_login_schema.go new file mode 100644 index 000000000..21389e9ca --- /dev/null +++ b/internal/schema/user_external_login_schema.go @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +// UserExternalLoginResp user external login resp +type UserExternalLoginResp struct { + BindingKey string `json:"binding_key"` + AccessToken string `json:"access_token"` + // ErrMsg error message, if not empty, means login failed and this message should be displayed. + ErrMsg string `json:"-"` + ErrTitle string `json:"-"` +} + +// ExternalLoginBindingUserSendEmailReq external login binding user request +type ExternalLoginBindingUserSendEmailReq struct { + BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"` + Email string `validate:"required,gt=1,lte=512,email" json:"email"` + // If must is true, whatever email if exists, try to bind user. + // If must is false, when email exist, will only be prompted with a warning. + Must bool `json:"must"` +} + +// ExternalLoginBindingUserSendEmailResp external login binding user response +type ExternalLoginBindingUserSendEmailResp struct { + EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"` + AccessToken string `json:"access_token"` +} + +// ExternalLoginBindingUserReq external login binding user request +type ExternalLoginBindingUserReq struct { + Code string `validate:"required,gt=0,lte=500" json:"code"` + Content string `json:"-"` +} + +// ExternalLoginBindingUserResp external login binding user response +type ExternalLoginBindingUserResp struct { + AccessToken string `json:"access_token"` +} + +// ExternalLoginUserInfoCache external login user info +type ExternalLoginUserInfoCache struct { + // Third party identification + // e.g. facebook, twitter, instagram + Provider string + // required. The unique user ID provided by the third-party login + ExternalID string + // optional. This name is used preferentially during registration + DisplayName string + // optional. This username is used preferentially during registration + Username string + // optional. If email exist will bind the existing user + Email string + // optional. The avatar URL provided by the third-party login platform + Avatar string + // optional. The original user information provided by the third-party login platform + MetaInfo string + // optional. The bio provided by the third-party login platform + Bio string +} + +// ExternalLoginUnbindingReq external login unbinding user +type ExternalLoginUnbindingReq struct { + ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"` + UserID string `json:"-"` +} + +// UserCenterUserSettingsResp user center user info response +type UserCenterUserSettingsResp struct { + ProfileSettingAgent UserSettingAgent `json:"profile_setting_agent"` + AccountSettingAgent UserSettingAgent `json:"account_setting_agent"` +} + +type UserCenterAdminFunctionAgentResp struct { + AllowCreateUser bool `json:"allow_create_user"` + AllowUpdateUserStatus bool `json:"allow_update_user_status"` + AllowUpdateUserPassword bool `json:"allow_update_user_password"` + AllowUpdateUserRole bool `json:"allow_update_user_role"` +} + +type UserSettingAgent struct { + Enabled bool `json:"enabled"` + RedirectURL string `json:"redirect_url"` +} diff --git a/internal/schema/user_notification_schema.go b/internal/schema/user_notification_schema.go new file mode 100644 index 000000000..eca97e81c --- /dev/null +++ b/internal/schema/user_notification_schema.go @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import ( + "encoding/json" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" +) + +type NotificationChannelConfig struct { + Key constant.NotificationChannelKey `json:"key"` + Enable bool `json:"enable"` +} + +type NotificationChannels []*NotificationChannelConfig + +func NewNotificationChannelsFormJson(jsonStr string) NotificationChannels { + var list NotificationChannels + _ = json.Unmarshal([]byte(jsonStr), &list) + return list +} + +func NewNotificationChannelConfigFormJson(jsonStr string) NotificationChannelConfig { + var list NotificationChannels + _ = json.Unmarshal([]byte(jsonStr), &list) + if len(list) > 0 { + return *list[0] + } + return NotificationChannelConfig{} +} + +func (n *NotificationChannels) ToJsonString() string { + data, _ := json.Marshal(n) + return string(data) +} + +type NotificationConfig struct { + Inbox NotificationChannelConfig `json:"inbox"` + AllNewQuestion NotificationChannelConfig `json:"all_new_question"` + AllNewQuestionForFollowingTags NotificationChannelConfig `json:"all_new_question_for_following_tags"` +} + +func NewNotificationConfig(configs []*entity.UserNotificationConfig) NotificationConfig { + nc := NotificationConfig{} + for _, item := range configs { + switch item.Source { + case string(constant.InboxSource): + nc.Inbox = NewNotificationChannelConfigFormJson(item.Channels) + case string(constant.AllNewQuestionSource): + nc.AllNewQuestion = NewNotificationChannelConfigFormJson(item.Channels) + case string(constant.AllNewQuestionForFollowingTagsSource): + nc.AllNewQuestionForFollowingTags = NewNotificationChannelConfigFormJson(item.Channels) + } + } + return nc +} + +func (n *NotificationConfig) Format() { + if n.Inbox.Key == "" { + n.Inbox.Key = constant.EmailChannel + n.Inbox.Enable = false + } + if n.AllNewQuestion.Key == "" { + n.AllNewQuestion.Key = constant.EmailChannel + n.AllNewQuestion.Enable = false + } + if n.AllNewQuestionForFollowingTags.Key == "" { + n.AllNewQuestionForFollowingTags.Key = constant.EmailChannel + n.AllNewQuestionForFollowingTags.Enable = false + } +} + +// UpdateUserNotificationConfigReq update user notification config request +type UpdateUserNotificationConfigReq struct { + NotificationConfig + UserID string `json:"-"` +} + +// GetUserNotificationConfigResp get user notification config response +type GetUserNotificationConfigResp struct { + NotificationConfig +} diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 757926c79..7ba8817a8 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -1,15 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema import ( + "context" "encoding/json" - "regexp" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/validator" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/pkg/checker" - "github.com/jinzhu/copier" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/pkg/day" "github.com/segmentfault/pacman/errors" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/converter" + "github.com/jinzhu/copier" ) // UserVerifyEmailReq user verify email request @@ -20,8 +45,8 @@ type UserVerifyEmailReq struct { Content string `json:"-"` } -// GetUserResp get user response -type GetUserResp struct { +// UserLoginResp get user response +type UserLoginResp struct { // user id ID string `json:"id"` // create time @@ -55,35 +80,56 @@ type GetUserResp struct { // bio markdown Bio string `json:"bio"` // bio html - BioHtml string `json:"bio_html"` + BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location Location string `json:"location"` - // ip info - IPInfo string `json:"ip_info"` + // language + Language string `json:"language"` + // Color scheme + ColorScheme string `json:"color_scheme"` // access token AccessToken string `json:"access_token"` - // is admin - IsAdmin bool `json:"is_admin"` + // role id + RoleID int `json:"role_id"` // user status Status string `json:"status"` + // user have password + HavePassword bool `json:"have_password"` + // visit token + VisitToken string `json:"visit_token"` + // suspended until timestamp + SuspendedUntil int64 `json:"suspended_until"` } -func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { +func (r *UserLoginResp) ConvertFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() - statusShow, ok := UserStatusShow[userInfo.Status] - if ok { - r.Status = statusShow + r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) + r.HavePassword = len(userInfo.Pass) > 0 + if !userInfo.SuspendedUntil.IsZero() { + r.SuspendedUntil = userInfo.SuspendedUntil.Unix() } } -// GetUserStatusResp get user status info -type GetUserStatusResp struct { - // user status - Status string `json:"status"` +type GetCurrentLoginUserInfoResp struct { + *UserLoginResp + Avatar *AvatarInfo `json:"avatar"` +} + +func (r *GetCurrentLoginUserInfoResp) ConvertFromUserEntity(userInfo *entity.User) { + _ = copier.Copy(r, userInfo) + r.CreatedAt = userInfo.CreatedAt.Unix() + r.LastLoginDate = userInfo.LastLoginDate.Unix() + r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) + if len(r.ColorScheme) == 0 { + r.ColorScheme = constant.ColorSchemeDefault + } + if !userInfo.SuspendedUntil.IsZero() { + r.SuspendedUntil = userInfo.SuspendedUntil.Unix() + } } // GetOtherUserInfoByUsernameResp get user response @@ -114,187 +160,206 @@ type GetOtherUserInfoByUsernameResp struct { // bio markdown Bio string `json:"bio"` // bio html - BioHtml string `json:"bio_html"` + BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location - Location string `json:"location"` - // ip info - IPInfo string `json:"ip_info"` - // is admin - IsAdmin bool `json:"is_admin"` + Location string `json:"location"` Status string `json:"status"` StatusMsg string `json:"status_msg,omitempty"` + // suspended until timestamp + SuspendedUntil int64 `json:"suspended_until"` } -func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User) { +func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() - statusShow, ok := UserStatusShow[userInfo.Status] - if ok { - r.Status = statusShow + r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) + if !userInfo.SuspendedUntil.IsZero() { + r.SuspendedUntil = userInfo.SuspendedUntil.Unix() } - if userInfo.MailStatus == entity.EmailStatusToBeVerified { - statusMsgShow, ok := UserStatusShowMsg[11] - if ok { - r.StatusMsg = statusMsgShow - } - } else { - statusMsgShow, ok := UserStatusShowMsg[userInfo.Status] - if ok { - r.StatusMsg = statusMsgShow - } - } - + r.StatusMsg = "" } -const ( - Mail_State_Pass = 1 - Mail_State_Verifi = 2 - - Notice_Status_On = 1 - Notice_Status_Off = 2 - - //ActionRecord ReportType - ActionRecord_Type_Login = "login" - ActionRecord_Type_Email = "e_mail" - ActionRecord_Type_Find_Pass = "find_pass" -) +func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntityWithLang(ctx context.Context, userInfo *entity.User) { + _ = copier.Copy(r, userInfo) + r.CreatedAt = userInfo.CreatedAt.Unix() + r.LastLoginDate = userInfo.LastLoginDate.Unix() + r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) -var UserStatusShow = map[int]string{ - 1: "normal", - 9: "forbidden", - 10: "deleted", -} -var UserStatusShowMsg = map[int]string{ - 1: "", - 9: "This user was suspended forever. This user doesn’t meet a community guideline.", - 10: "This user was deleted.", - 11: "This user is inactive.", + lang := handler.GetLangByCtx(ctx) + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + r.StatusMsg = translator.Tr(lang, reason.UserStatusInactive) + } + switch userInfo.Status { + case entity.UserStatusSuspended: + if userInfo.SuspendedUntil.IsZero() || userInfo.SuspendedUntil.Year() >= 2099 { + r.StatusMsg = translator.Tr(lang, reason.UserStatusSuspendedForever) + } else { + r.SuspendedUntil = userInfo.SuspendedUntil.Unix() + trans := translator.GlobalTrans.Tr(lang, "ui.dates.long_date_with_time") + suspendedUntilFormatted := day.Format(userInfo.SuspendedUntil.Unix(), trans, "UTC") + r.StatusMsg = translator.TrWithData(lang, reason.UserStatusSuspendedUntil, map[string]interface{}{ + "SuspendedUntil": suspendedUntilFormatted, + }) + } + case entity.UserStatusDeleted: + r.StatusMsg = translator.Tr(lang, reason.UserStatusDeleted) + } } -// EmailLogin -type UserEmailLogin struct { - Email string `json:"e_mail" ` // e_mail - Pass string `json:"pass" ` // password - CaptchaID string `json:"captcha_id" ` // captcha_id - CaptchaCode string `json:"captcha_code" ` // captcha_code +// UserEmailLoginReq user email login request +type UserEmailLoginReq struct { + Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` + Pass string `validate:"required,gte=8,lte=32" json:"pass"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` } // UserRegisterReq user register request type UserRegisterReq struct { - // name - Name string `validate:"required,gt=4,lte=30" json:"name"` - // email - Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` - // password - Pass string `validate:"required,gte=8,lte=32" json:"pass"` - IP string `json:"-" ` -} - -func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) { - // TODO i18n - err = checker.CheckPassword(8, 32, 0, u.Pass) - if err != nil { - return &validator.ErrorField{ - Key: "pass", - Value: err.Error(), - }, err + Name string `validate:"required,gte=2,lte=30" json:"name"` + Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` + Pass string `validate:"required,gte=8,lte=32" json:"pass"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` + IP string `json:"-" ` +} + +func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) { + if err = checker.CheckPassword(u.Pass); err != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "pass", + ErrorMsg: err.Error(), + }) + return errFields, err } return nil, nil } -// UserModifyPassWordRequest -type UserModifyPassWordRequest struct { - UserId string `json:"-" ` // user_id - OldPass string `json:"old_pass" ` // old password - Pass string `json:"pass" ` // password +type UserModifyPasswordReq struct { + OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"` + Pass string `validate:"required,gte=8,lte=32" json:"pass"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` + UserID string `json:"-"` + AccessToken string `json:"-"` } -func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err error) { - // TODO i18n - err = checker.CheckPassword(8, 32, 0, u.Pass) - if err != nil { - return &validator.ErrorField{ - Key: "pass", - Value: err.Error(), - }, err +func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) { + if err = checker.CheckPassword(u.Pass); err != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "pass", + ErrorMsg: err.Error(), + }) + return errFields, err } return nil, nil } type UpdateInfoRequest struct { - // display_name - DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"` - // username - Username string `validate:"omitempty,gt=0,lte=30" json:"username"` - // avatar - Avatar string `validate:"omitempty,gt=0,lte=500" json:"avatar"` - // bio - Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"` - // bio - BioHtml string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"` - // website - Website string `validate:"omitempty,gt=0,lte=500" json:"website"` - // location - Location string `validate:"omitempty,gt=0,lte=100" json:"location"` + DisplayName string `validate:"omitempty,gte=2,lte=30" json:"display_name"` + Username string `validate:"omitempty,gte=2,lte=30" json:"username"` + Avatar AvatarInfo `json:"avatar"` + Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"` + BioHTML string `json:"-"` + Website string `validate:"omitempty,gt=0,lte=500" json:"website"` + Location string `validate:"omitempty,gt=0,lte=100" json:"location"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` +} + +type AvatarInfo struct { + Type string `validate:"omitempty,gt=0,lte=100" json:"type"` + Gravatar string `validate:"omitempty,gt=0,lte=200" json:"gravatar"` + Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"` +} + +func (a *AvatarInfo) ToJsonString() string { + data, _ := json.Marshal(a) + return string(data) +} + +func (a *AvatarInfo) GetURL() string { + switch a.Type { + case constant.AvatarTypeGravatar: + return a.Gravatar + case constant.AvatarTypeCustom: + return a.Custom + default: + return "" + } +} + +func CustomAvatar(url string) *AvatarInfo { + return &AvatarInfo{ + Type: constant.AvatarTypeCustom, + Custom: url, + } +} + +func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) { + req.BioHTML = converter.Markdown2BasicHTML(req.Bio) + if len(req.Website) > 0 && !checker.IsURL(req.Website) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "website", + ErrorMsg: reason.InvalidURLError, + }), errors.BadRequest(reason.InvalidURLError) + } + return nil, nil +} + +// UpdateUserInterfaceRequest update user interface request +type UpdateUserInterfaceRequest struct { + // language + Language string `validate:"required,gt=1,lte=100" json:"language"` + // Color scheme + ColorScheme string `validate:"required,gt=1,lte=100" json:"color_scheme"` // user id - UserId string `json:"-" ` -} - -func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) { - if len(u.Username) > 0 { - re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`) - match := re.MatchString(u.Username) - if !match { - err = errors.BadRequest(reason.UsernameInvalid) - return &validator.ErrorField{ - Key: "username", - Value: err.Error(), - }, err - } + UserId string `json:"-"` +} + +func (req *UpdateUserInterfaceRequest) Check() (errFields []*validator.FormErrorField, err error) { + if !translator.CheckLanguageIsValid(req.Language) { + return nil, errors.BadRequest(reason.LangNotFound) + } + if req.ColorScheme != constant.ColorSchemeDefault && + req.ColorScheme != constant.ColorSchemeLight && + req.ColorScheme != constant.ColorSchemeDark && + req.ColorScheme != constant.ColorSchemeSystem { + req.ColorScheme = constant.ColorSchemeDefault } return nil, nil } type UserRetrievePassWordRequest struct { - Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail - CaptchaID string `json:"captcha_id" ` // captcha_id - CaptchaCode string `json:"captcha_code" ` // captcha_code + Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` } type UserRePassWordRequest struct { - Code string `validate:"required,gt=0,lte=100" json:"code" ` // code - Pass string `validate:"required,gt=0,lte=32" json:"pass" ` // Password + Code string `validate:"required,gt=0,lte=100" json:"code"` + Pass string `validate:"required,gt=0,lte=32" json:"pass"` Content string `json:"-"` } -func (u *UserRePassWordRequest) Check() (errField *validator.ErrorField, err error) { - // TODO i18n - err = checker.CheckPassword(8, 32, 0, u.Pass) - if err != nil { - return &validator.ErrorField{ - Key: "pass", - Value: err.Error(), - }, err +func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) { + if err = checker.CheckPassword(u.Pass); err != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "pass", + ErrorMsg: err.Error(), + }) + return errFields, err } return nil, nil } -type UserNoticeSetRequest struct { - UserId string `json:"-" ` // user_id - NoticeSwitch bool `json:"notice_switch" ` -} - -type UserNoticeSetResp struct { - NoticeSwitch bool `json:"notice_switch"` -} - type ActionRecordReq struct { - // action - Action string `validate:"required,oneof=login e_mail find_pass" form:"action"` - Ip string `json:"-"` + Action string `validate:"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote" form:"action"` + IP string `json:"-"` + UserID string `json:"-"` } type ActionRecordResp struct { @@ -304,56 +369,84 @@ type ActionRecordResp struct { } type UserBasicInfo struct { - ID string `json:"-" ` // user_id - Username string `json:"username" ` // name - Rank int `json:"rank" ` // rank - DisplayName string `json:"display_name"` // display_name - Avatar string `json:"avatar" ` // avatar - Website string `json:"website" ` // website - Location string `json:"location" ` // location - IpInfo string `json:"ip_info"` // ip info - Status string `json:"status"` // status + ID string `json:"id"` + Username string `json:"username"` + Rank int `json:"rank"` + DisplayName string `json:"display_name"` + Avatar string `json:"avatar"` + Website string `json:"website"` + Location string `json:"location"` + Language string `json:"language"` + Status string `json:"status"` + SuspendedUntil int64 `json:"suspended_until"` } type GetOtherUserInfoByUsernameReq struct { Username string `validate:"required,gt=0,lte=500" form:"username"` + UserID string `json:"-"` + IsAdmin bool `json:"-"` } type GetOtherUserInfoResp struct { Info *GetOtherUserInfoByUsernameResp `json:"info"` - Has bool `json:"has"` } type UserChangeEmailSendCodeReq struct { + UserVerifyEmailSendReq Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` + Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"` UserID string `json:"-"` } -type EmailCodeContent struct { - Email string `json:"e_mail"` - UserID string `json:"user_id"` +type UserChangeEmailVerifyReq struct { + Code string `validate:"required,gt=0,lte=500" json:"code"` + Content string `json:"-"` +} + +type UserVerifyEmailSendReq struct { + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` } -func (r *EmailCodeContent) ToJSONString() string { - codeBytes, _ := json.Marshal(r) - return string(codeBytes) +// UserRankingResp user ranking response +type UserRankingResp struct { + UsersWithTheMostReputation []*UserRankingSimpleInfo `json:"users_with_the_most_reputation"` + UsersWithTheMostVote []*UserRankingSimpleInfo `json:"users_with_the_most_vote"` + Staffs []*UserRankingSimpleInfo `json:"staffs"` } -func (r *EmailCodeContent) FromJSONString(data string) error { - return json.Unmarshal([]byte(data), &r) +// UserRankingSimpleInfo user ranking simple info +type UserRankingSimpleInfo struct { + // username + Username string `json:"username"` + // rank + Rank int `json:"rank"` + // vote + VoteCount int `json:"vote_count"` + // display name + DisplayName string `json:"display_name"` + // avatar + Avatar string `json:"avatar"` } -type UserChangeEmailVerifyReq struct { +// UserUnsubscribeNotificationReq user unsubscribe email notification request +type UserUnsubscribeNotificationReq struct { Code string `validate:"required,gt=0,lte=500" json:"code"` Content string `json:"-"` } -type UserVerifyEmailSendReq struct { - CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"` - CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"` +// GetUserStaffReq get user staff request +type GetUserStaffReq struct { + Username string `validate:"omitempty,gt=0,lte=500" form:"username"` + PageSize int `validate:"omitempty,min=1" form:"page_size"` } -type UserVerifyEmailErrorResponse struct { - Key string `json:"key"` - Value string `json:"value"` +// GetUserStaffResp get user staff response +type GetUserStaffResp struct { + // username + Username string `json:"username"` + // display name + DisplayName string `json:"display_name"` + // avatar + Avatar string `json:"avatar"` } diff --git a/internal/schema/vote_schema.go b/internal/schema/vote_schema.go index ec37ebc87..e82adcc2f 100644 --- a/internal/schema/vote_schema.go +++ b/internal/schema/vote_schema.go @@ -1,24 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package schema type VoteReq struct { - ObjectID string `validate:"required" form:"object_id" json:"object_id"` // id - IsCancel bool `validate:"omitempty" form:"is_cancel" json:"is_cancel"` // is cancel + ObjectID string `validate:"required" json:"object_id"` + IsCancel bool `validate:"omitempty" json:"is_cancel"` + CaptchaID string `json:"captcha_id"` + CaptchaCode string `json:"captcha_code"` + UserID string `json:"-"` +} + +type VoteResp struct { + UpVotes int64 `json:"up_votes"` + DownVotes int64 `json:"down_votes"` + Votes int64 `json:"votes"` + VoteStatus string `json:"vote_status"` } -type VoteDTO struct { - // object TagID +// VoteOperationInfo vote operation info +type VoteOperationInfo struct { + // operation object id ObjectID string - // is cancel - IsCancel bool - // user TagID - UserID string + // question answer comment + ObjectType string + // object owner user id + ObjectCreatorUserID string + // operation user id + OperatingUserID string + // vote up + VoteUp bool + // vote down + VoteDown bool + // vote activity info + Activities []*VoteActivity } -type VoteResp struct { - UpVotes int `json:"up_votes"` - DownVotes int `json:"down_votes"` - Votes int `json:"votes"` - VoteStatus string `json:"vote_status"` +// VoteActivity vote activity +type VoteActivity struct { + ActivityType int + ActivityUserID string + TriggerUserID string + Rank int +} + +func (v *VoteActivity) HasRank() int { + if v.Rank != 0 { + return 1 + } + return 0 } type GetVoteWithPageReq struct { @@ -27,23 +73,7 @@ type GetVoteWithPageReq struct { // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` // user id - UserID string `validate:"required" form:"user_id"` -} - -type VoteQuestion struct { - // object ID - ID string `json:"id"` - // title - Title string `json:"title"` -} - -type VoteAnswer struct { - // object ID - ID string `json:"id"` - // question ID - QuestionID string `json:"question_id"` - // title - Title string `json:"title"` + UserID string `json:"-"` } type GetVoteWithPageResp struct { @@ -59,6 +89,8 @@ type GetVoteWithPageResp struct { ObjectType string `json:"object_type" enums:"question,answer,tag,comment"` // title Title string `json:"title"` + // url title + UrlTitle string `json:"url_title"` // content Content string `json:"content"` // vote type diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go index 380e2a8c5..04f8b16ac 100644 --- a/internal/service/action/captcha_service.go +++ b/internal/service/action/captcha_service.go @@ -1,14 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package action import ( "context" - "image/color" - "strings" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - "github.com/mojocn/base64Captcha" - "github.com/segmentfault/pacman/errors" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/log" ) @@ -16,9 +33,10 @@ import ( type CaptchaRepo interface { SetCaptcha(ctx context.Context, key, captcha string) (err error) GetCaptcha(ctx context.Context, key string) (captcha string, err error) - SetActionType(ctx context.Context, ip, actionType string, amount int) (err error) - GetActionType(ctx context.Context, ip, actionType string) (amount int, err error) - DelActionType(ctx context.Context, ip, actionType string) (err error) + DelCaptcha(ctx context.Context, key string) (err error) + SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) + GetActionType(ctx context.Context, unit, actionType string) (actioninfo *entity.ActionRecordInfo, err error) + DelActionType(ctx context.Context, unit, actionType string) (err error) } // CaptchaService kit service @@ -36,14 +54,38 @@ func NewCaptchaService(captchaRepo CaptchaRepo) *CaptchaService { // ActionRecord action record func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRecordReq) (resp *schema.ActionRecordResp, err error) { resp = &schema.ActionRecordResp{} - num, err := cs.captchaRepo.GetActionType(ctx, req.Ip, req.Action) - if err != nil { - num = 0 + unit := req.IP + switch req.Action { + case entity.CaptchaActionEditUserinfo: + unit = req.UserID + case entity.CaptchaActionQuestion: + unit = req.UserID + case entity.CaptchaActionAnswer: + unit = req.UserID + case entity.CaptchaActionComment: + unit = req.UserID + case entity.CaptchaActionEdit: + unit = req.UserID + case entity.CaptchaActionInvitationAnswer: + unit = req.UserID + case entity.CaptchaActionSearch: + if req.UserID != "" { + unit = req.UserID + } + case entity.CaptchaActionReport: + unit = req.UserID + case entity.CaptchaActionDelete: + unit = req.UserID + case entity.CaptchaActionVote: + unit = req.UserID } - // TODO config num to config file - if num >= 3 { - resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx) + verificationResult := cs.ValidationStrategy(ctx, unit, req.Action) + if !verificationResult { resp.Verify = true + resp.CaptchaID, resp.CaptchaImg, err = cs.GenerateCaptcha(ctx) + if err != nil { + log.Errorf("GenerateCaptcha error: %v", err) + } } return } @@ -51,37 +93,38 @@ func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRe // ActionRecordVerifyCaptcha // Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct func (cs *CaptchaService) ActionRecordVerifyCaptcha( - ctx context.Context, actionType string, ip string, id string, VerifyValue string) bool { - num, cahceErr := cs.captchaRepo.GetActionType(ctx, ip, actionType) - if cahceErr != nil { + ctx context.Context, actionType string, unit string, captchaID string, captchaCode string, +) bool { + verificationResult := cs.ValidationStrategy(ctx, unit, actionType) + if verificationResult { return true } - if num >= 3 { - pass, err := cs.VerifyCaptcha(ctx, id, VerifyValue) - if err != nil { - return false - } - return pass + pass, err := cs.VerifyCaptcha(ctx, captchaID, captchaCode) + if err != nil { + return false } - return true + return pass } -func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, ip string) (int, error) { - var err error - num, cahceErr := cs.captchaRepo.GetActionType(ctx, ip, actionType) - if cahceErr != nil { +func (cs *CaptchaService) ActionRecordAdd(ctx context.Context, actionType string, unit string) (int, error) { + info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType) + if err != nil { log.Error(err) + return 0, err } - num++ - err = cs.captchaRepo.SetActionType(ctx, ip, actionType, num) + amount := 1 + if info != nil { + amount = info.Num + 1 + } + err = cs.captchaRepo.SetActionType(ctx, unit, actionType, "", amount) if err != nil { return 0, err } - return num, nil + return amount, nil } -func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, ip string) { - err := cs.captchaRepo.DelActionType(ctx, ip, actionType) +func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string, unit string) { + err := cs.captchaRepo.DelActionType(ctx, unit, actionType) if err != nil { log.Error(err) } @@ -89,37 +132,32 @@ func (cs *CaptchaService) ActionRecordDel(ctx context.Context, actionType string // GenerateCaptcha generate captcha func (cs *CaptchaService) GenerateCaptcha(ctx context.Context) (key, captchaBase64 string, err error) { - driverString := base64Captcha.DriverString{ - Height: 40, - Width: 100, - NoiseCount: 0, - ShowLineOptions: 2 | 4, - Length: 4, - Source: "1234567890qwertyuioplkjhgfdsazxcvbnm", - BgColor: &color.RGBA{R: 3, G: 102, B: 214, A: 125}, - Fonts: []string{"wqy-microhei.ttc"}, - } - driver := driverString.ConvertFonts() - - id, content, answer := driver.GenerateIdQuestionAnswer() - item, err := driver.DrawCaptcha(content) - if err != nil { - return "", "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() - } - err = cs.captchaRepo.SetCaptcha(ctx, id, answer) - if err != nil { - return "", "", err + realCaptcha := "" + key = token.GenerateToken() + _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { + if captcha, code := fn.Create(); len(code) > 0 { + captchaBase64 = captcha + realCaptcha = code + } + return nil + }) + if len(realCaptcha) == 0 { + return key, captchaBase64, nil } - captchaBase64 = item.EncodeB64string() - return id, captchaBase64, nil + err = cs.captchaRepo.SetCaptcha(ctx, key, realCaptcha) + return key, captchaBase64, err } // VerifyCaptcha generate captcha func (cs *CaptchaService) VerifyCaptcha(ctx context.Context, key, captcha string) (isCorrect bool, err error) { - realCaptcha, err := cs.captchaRepo.GetCaptcha(ctx, key) - if err != nil { - return false, nil - } - return strings.TrimSpace(captcha) == realCaptcha, nil + realCaptcha, _ := cs.captchaRepo.GetCaptcha(ctx, key) + + _ = plugin.CallCaptcha(func(fn plugin.Captcha) error { + isCorrect = fn.Verify(realCaptcha, captcha) + return nil + }) + + _ = cs.captchaRepo.DelCaptcha(ctx, key) + return isCorrect, nil } diff --git a/internal/service/action/captcha_strategy.go b/internal/service/action/captcha_strategy.go new file mode 100644 index 000000000..3befda821 --- /dev/null +++ b/internal/service/action/captcha_strategy.go @@ -0,0 +1,225 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package action + +import ( + "context" + "time" + + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/internal/entity" +) + +// ValidationStrategy +// true pass +// false need captcha +func (cs *CaptchaService) ValidationStrategy(ctx context.Context, unit, actionType string) bool { + // If the captcha is not enabled, the verification is passed directly + if !plugin.CaptchaEnabled() { + return true + } + info, err := cs.captchaRepo.GetActionType(ctx, unit, actionType) + if err != nil { + log.Error(err) + return false + } + switch actionType { + case entity.CaptchaActionEmail: + return cs.CaptchaActionEmail(ctx, unit, info) + case entity.CaptchaActionPassword: + return cs.CaptchaActionPassword(ctx, unit, info) + case entity.CaptchaActionEditUserinfo: + return cs.CaptchaActionEditUserinfo(ctx, unit, info) + case entity.CaptchaActionQuestion: + return cs.CaptchaActionQuestion(ctx, unit, info) + case entity.CaptchaActionAnswer: + return cs.CaptchaActionAnswer(ctx, unit, info) + case entity.CaptchaActionComment: + return cs.CaptchaActionComment(ctx, unit, info) + case entity.CaptchaActionEdit: + return cs.CaptchaActionEdit(ctx, unit, info) + case entity.CaptchaActionInvitationAnswer: + return cs.CaptchaActionInvitationAnswer(ctx, unit, info) + case entity.CaptchaActionSearch: + return cs.CaptchaActionSearch(ctx, unit, info) + case entity.CaptchaActionReport: + return cs.CaptchaActionReport(ctx, unit, info) + case entity.CaptchaActionDelete: + return cs.CaptchaActionDelete(ctx, unit, info) + case entity.CaptchaActionVote: + return cs.CaptchaActionVote(ctx, unit, info) + + } + //actionType not found + return false +} + +func (cs *CaptchaService) CaptchaActionEmail(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + // You need a verification code every time + return false +} + +func (cs *CaptchaService) CaptchaActionPassword(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 3 + setTime := int64(60 * 30) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum { + return false + } + if now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionPassword, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionEditUserinfo(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 3 + setTime := int64(60 * 30) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime && actionInfo.Num >= setNum { + return false + } + if now-actionInfo.LastTime != 0 && now-actionInfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionEditUserinfo, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionQuestion(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 10 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 10 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionComment(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 30 + setTime := int64(1) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionEdit(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 10 + if actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionInvitationAnswer(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 30 + if actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionSearch(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + now := time.Now().Unix() + setNum := 20 + setTime := int64(60) //seconds + if now-int64(actionInfo.LastTime) <= setTime && actionInfo.Num >= setNum { + return false + } + if now-actionInfo.LastTime > setTime { + cs.captchaRepo.SetActionType(ctx, unit, entity.CaptchaActionSearch, "", 0) + } + return true +} + +func (cs *CaptchaService) CaptchaActionReport(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 30 + setTime := int64(1) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionDelete(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 5 + setTime := int64(5) //seconds + now := time.Now().Unix() + if now-actionInfo.LastTime <= setTime || actionInfo.Num >= setNum { + return false + } + return true +} + +func (cs *CaptchaService) CaptchaActionVote(ctx context.Context, unit string, actionInfo *entity.ActionRecordInfo) bool { + if actionInfo == nil { + return true + } + setNum := 40 + if actionInfo.Num >= setNum { + return false + } + return true +} diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go new file mode 100644 index 000000000..2d384f34c --- /dev/null +++ b/internal/service/activity/activity.go @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/meta_common" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/log" +) + +// ActivityRepo activity repository +type ActivityRepo interface { + GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (activityList []*entity.Activity, err error) +} + +// ActivityService activity service +type ActivityService struct { + activityRepo ActivityRepo + userCommon *usercommon.UserCommon + activityCommonService *activity_common.ActivityCommon + tagCommonService *tag_common.TagCommonService + objectInfoService *object_info.ObjService + commentCommonService *comment_common.CommentCommonService + revisionService *revision_common.RevisionService + metaService *metacommon.MetaCommonService + configService *config.ConfigService +} + +// NewActivityService new activity service +func NewActivityService( + activityRepo ActivityRepo, + userCommon *usercommon.UserCommon, + activityCommonService *activity_common.ActivityCommon, + tagCommonService *tag_common.TagCommonService, + objectInfoService *object_info.ObjService, + commentCommonService *comment_common.CommentCommonService, + revisionService *revision_common.RevisionService, + metaService *metacommon.MetaCommonService, + configService *config.ConfigService, +) *ActivityService { + return &ActivityService{ + objectInfoService: objectInfoService, + activityRepo: activityRepo, + userCommon: userCommon, + activityCommonService: activityCommonService, + tagCommonService: tagCommonService, + commentCommonService: commentCommonService, + revisionService: revisionService, + metaService: metaService, + configService: configService, + } +} + +// GetObjectTimeline get object timeline +func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.GetObjectTimelineReq) ( + resp *schema.GetObjectTimelineResp, err error) { + resp = &schema.GetObjectTimelineResp{ + ObjectInfo: &schema.ActObjectInfo{}, + Timeline: make([]*schema.ActObjectTimeline, 0), + } + + resp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID) + if err != nil { + return nil, err + } + + activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote) + if err != nil { + return nil, err + } + for _, act := range activityList { + item := &schema.ActObjectTimeline{ + ActivityID: act.ID, + RevisionID: converter.IntToString(act.RevisionID), + CreatedAt: act.CreatedAt.Unix(), + Cancelled: act.Cancelled == entity.ActivityCancelled, + ObjectID: act.ObjectID, + UserInfo: &schema.UserBasicInfo{}, + } + item.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID) + if item.Cancelled { + item.CancelledAt = act.CancelledAt.Unix() + } + + if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType { + if handler.GetEnableShortID(ctx) { + item.ObjectID = uid.EnShortID(act.ObjectID) + } + } + + cfg, err := as.configService.GetConfigByID(ctx, act.ActivityType) + if err != nil { + log.Errorf("fail to get config by id: %d, err: %v, act id is: %s", act.ActivityType, err, act.ID) + } else { + // database save activity type is number, change to activity type string is like "question.asked". + // so we need to cut the front part of '.', only need string like 'asked' + _, item.ActivityType, _ = strings.Cut(cfg.Key, ".") + // format activity type string to show + if isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden { + continue + } else { + item.ActivityType = formattedActivityType + } + } + + // if activity is down vote, only admin can see who does it. + if item.ActivityType == constant.ActDownVote && !req.IsAdmin { + item.UserInfo.Username = "N/A" + item.UserInfo.DisplayName = "N/A" + } else { + if act.TriggerUserID > 0 { + item.UserInfo.ID = fmt.Sprintf("%d", act.TriggerUserID) + } else { + item.UserInfo.ID = act.UserID + } + } + + item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID) + resp.Timeline = append(resp.Timeline, item) + } + as.formatTimelineUserInfo(ctx, resp.Timeline) + return +} + +func (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) ( + resp *schema.ActObjectInfo, err error) { + resp = &schema.ActObjectInfo{} + objInfo, err := as.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return nil, err + } + resp.Title = objInfo.Title + if objInfo.ObjectType == constant.TagObjectType { + tag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID) + if exist { + resp.Title = tag.SlugName + resp.MainTagSlugName = tag.MainTagSlugName + } + } + resp.ObjectType = objInfo.ObjectType + resp.QuestionID = objInfo.QuestionID + resp.AnswerID = objInfo.AnswerID + if len(objInfo.ObjectCreatorUserID) > 0 { + // get object creator user info + userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, objInfo.ObjectCreatorUserID) + if err != nil { + return nil, err + } + if exist { + resp.Username = userBasicInfo.Username + resp.DisplayName = userBasicInfo.DisplayName + } + } + return resp, nil +} + +func (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType, + activityType, revisionID string) (comment string) { + if objectType == constant.CommentObjectType { + commentInfo, err := as.commentCommonService.GetComment(ctx, objectID) + if err != nil { + log.Error(err) + } else { + return commentInfo.ParsedText + } + return + } + + if activityType == constant.ActEdited { + revision, err := as.revisionService.GetRevision(ctx, revisionID) + if err != nil { + log.Error(err) + } else { + return converter.Markdown2HTML(revision.Log) + } + return + } + if activityType == constant.ActClosed { + // only question can be closed + metaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey) + if err != nil { + log.Error(err) + } else { + closeMsg := &schema.CloseQuestionMeta{} + if err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil { + return converter.Markdown2HTML(closeMsg.CloseMsg) + } + } + } + return "" +} + +func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) { + userExist := make(map[string]bool) + userIDs := make([]string, 0) + for _, info := range timeline { + if len(info.UserInfo.ID) == 0 || userExist[info.UserInfo.ID] { + continue + } + userIDs = append(userIDs, info.UserInfo.ID) + } + if len(userIDs) == 0 { + return + } + userInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs) + if err != nil { + log.Error(err) + return + } + for _, info := range timeline { + if len(info.UserInfo.ID) == 0 { + continue + } + info.UserInfo = userInfoMapping[info.UserInfo.ID] + } +} + +// GetObjectTimelineDetail get object timeline +func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) ( + resp *schema.GetObjectTimelineDetailResp, err error) { + resp = &schema.GetObjectTimelineDetailResp{} + resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID) + resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID) + return resp, nil +} + +// getOneObjectDetail get object detail +func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) ( + resp *schema.ObjectTimelineDetail, err error) { + resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)} + + // if request revision is 0, return null object detail. + if revisionID == "0" { + return nil, nil + } + + revision, err := as.revisionService.GetRevision(ctx, revisionID) + if err != nil { + log.Warn(err) + return nil, nil + } + objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID) + if err != nil { + return nil, err + } + + switch objInfo.ObjectType { + case constant.QuestionObjectType: + data := &entity.QuestionWithTagsRevision{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + for _, tag := range data.Tags { + resp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{ + SlugName: tag.SlugName, + DisplayName: tag.DisplayName, + MainTagSlugName: tag.MainTagSlugName, + Recommend: tag.Recommend, + Reserved: tag.Reserved, + }) + } + resp.Title = data.Title + resp.OriginalText = data.OriginalText + case constant.AnswerObjectType: + data := &entity.Answer{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + resp.Title = objInfo.Title // answer show question title + resp.OriginalText = data.OriginalText + case constant.TagObjectType: + data := &entity.Tag{} + if err = json.Unmarshal([]byte(revision.Content), data); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + resp.Title = data.DisplayName + resp.OriginalText = data.OriginalText + resp.SlugName = data.SlugName + resp.MainTagSlugName = data.MainTagSlugName + default: + log.Errorf("unknown object type %s", objInfo.ObjectType) + } + return resp, nil +} + +func formatActivity(activityType string) (isHidden bool, formattedActivityType string) { + if activityType == constant.ActVotedUp || + activityType == constant.ActVotedDown || + activityType == constant.ActFollow { + return true, "" + } + if activityType == constant.ActVoteUp { + return false, constant.ActUpVote + } + if activityType == constant.ActVoteDown { + return false, constant.ActDownVote + } + if activityType == constant.ActAccepted { + return false, constant.ActAccept + } + return false, activityType +} diff --git a/internal/service/activity/answer_activity.go b/internal/service/activity/answer_activity.go deleted file mode 100644 index 30933f084..000000000 --- a/internal/service/activity/answer_activity.go +++ /dev/null @@ -1,77 +0,0 @@ -package activity - -import ( - "context" - "time" - - "github.com/segmentfault/pacman/log" -) - -// AnswerActivityRepo answer activity -type AnswerActivityRepo interface { - AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) - CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string) (err error) - DeleteAnswer(ctx context.Context, answerID string) (err error) -} - -// QuestionActivityRepo answer activity -type QuestionActivityRepo interface { - DeleteQuestion(ctx context.Context, questionID string) (err error) -} - -// AnswerActivityService user service -type AnswerActivityService struct { - answerActivityRepo AnswerActivityRepo - questionActivityRepo QuestionActivityRepo -} - -// NewAnswerActivityService new comment service -func NewAnswerActivityService( - answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService { - return &AnswerActivityService{ - answerActivityRepo: answerActivityRepo, - questionActivityRepo: questionActivityRepo, - } -} - -// AcceptAnswer accept answer change activity -func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) { - return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionUserID, answerUserID, isSelf) -} - -// CancelAcceptAnswer cancel accept answer change activity -func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string) (err error) { - return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionUserID, answerUserID) -} - -// DeleteAnswer delete answer change activity -func (as *AnswerActivityService) DeleteAnswer(ctx context.Context, answerID string, createdAt time.Time, - voteCount int) (err error) { - if voteCount >= 3 { - log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", answerID, voteCount) - return nil - } - if createdAt.Before(time.Now().AddDate(0, 0, -60)) { - log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", answerID, createdAt.String()) - return nil - } - return as.answerActivityRepo.DeleteAnswer(ctx, answerID) -} - -// DeleteQuestion delete question change activity -func (as *AnswerActivityService) DeleteQuestion(ctx context.Context, questionID string, createdAt time.Time, - voteCount int) (err error) { - if voteCount >= 3 { - log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", questionID, voteCount) - return nil - } - if createdAt.Before(time.Now().AddDate(0, 0, -60)) { - log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", questionID, createdAt.String()) - return nil - } - return as.questionActivityRepo.DeleteQuestion(ctx, questionID) -} diff --git a/internal/service/activity/answer_activity_service.go b/internal/service/activity/answer_activity_service.go new file mode 100644 index 000000000..169e7eb39 --- /dev/null +++ b/internal/service/activity/answer_activity_service.go @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity + +import ( + "context" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_type" + "github.com/apache/answer/internal/service/config" + "github.com/segmentfault/pacman/log" +) + +// AnswerActivityRepo answer activity +type AnswerActivityRepo interface { + SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) + SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error) +} + +// AnswerActivityService answer activity service +type AnswerActivityService struct { + answerActivityRepo AnswerActivityRepo + configService *config.ConfigService +} + +// NewAnswerActivityService new comment service +func NewAnswerActivityService( + answerActivityRepo AnswerActivityRepo, + configService *config.ConfigService, +) *AnswerActivityService { + return &AnswerActivityService{ + answerActivityRepo: answerActivityRepo, + configService: configService, + } +} + +// AcceptAnswer accept answer change activity +func (as *AnswerActivityService) AcceptAnswer(ctx context.Context, + loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) { + log.Debugf("user %s want to accept answer %s[%s] for question %s[%s]", loginUserID, + answerObjID, answerUserID, + questionObjID, questionUserID) + operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID, isSelf) + return as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo) +} + +// CancelAcceptAnswer cancel accept answer change activity +func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context, + loginUserID, answerObjID, questionObjID, questionUserID, answerUserID string) (err error) { + operationInfo := as.createAcceptAnswerOperationInfo(ctx, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID, false) + return as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo) +} + +func (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context, loginUserID, + answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo { + operationInfo := &schema.AcceptAnswerOperationInfo{ + TriggerUserID: loginUserID, + QuestionObjectID: questionObjID, + QuestionUserID: questionUserID, + AnswerObjectID: answerObjID, + AnswerUserID: answerUserID, + } + operationInfo.Activities = as.getActivities(ctx, operationInfo) + if isSelf { + for _, activity := range operationInfo.Activities { + activity.Rank = 0 + } + } + return operationInfo +} + +func (as *AnswerActivityService) getActivities(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ( + activities []*schema.AcceptAnswerActivity) { + activities = make([]*schema.AcceptAnswerActivity, 0) + + for _, action := range []string{activity_type.AnswerAccept, activity_type.AnswerAccepted} { + t := &schema.AcceptAnswerActivity{} + cfg, err := as.configService.GetConfigByKey(ctx, action) + if err != nil { + log.Warnf("get config by key error: %v", err) + continue + } + t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() + + if action == activity_type.AnswerAccept { + t.ActivityUserID = op.QuestionUserID + t.TriggerUserID = op.TriggerUserID + t.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer. + } else { + t.ActivityUserID = op.AnswerUserID + t.TriggerUserID = op.TriggerUserID + t.OriginalObjectID = op.AnswerObjectID // if activity is 'accepted' means this answer was accepted. + } + activities = append(activities, t) + } + return activities +} diff --git a/internal/service/activity/review_active.go b/internal/service/activity/review_active.go new file mode 100644 index 000000000..48ad5035c --- /dev/null +++ b/internal/service/activity/review_active.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity + +import ( + "context" + + "github.com/apache/answer/internal/schema" +) + +// ReviewActivityRepo interface +type ReviewActivityRepo interface { + Review(ctx context.Context, sct *schema.PassReviewActivity) (err error) +} diff --git a/internal/service/activity/user_active.go b/internal/service/activity/user_active.go index 356d91ee1..863c7ae49 100644 --- a/internal/service/activity/user_active.go +++ b/internal/service/activity/user_active.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity import "context" diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index 0b35c4784..74f73a755 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -1,16 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity_common import ( "context" + "time" - "github.com/answerdev/answer/internal/entity" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) type ActivityRepo interface { GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error) - GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) + GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) ( existsActivity *entity.Activity, exist bool, err error) + GetUserActivitiesByActivityType(ctx context.Context, userID string, activityType int) (activityList []*entity.Activity, err error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) + GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) + AddActivity(ctx context.Context, activity *entity.Activity) (err error) + GetUsersWhoHasGainedTheMostReputation( + ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) + GetUsersWhoHasVoteMost( + ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) +} + +type ActivityCommon struct { + activityRepo ActivityRepo + activityQueueService activity_queue.ActivityQueueService +} + +// NewActivityCommon new activity common +func NewActivityCommon( + activityRepo ActivityRepo, + activityQueueService activity_queue.ActivityQueueService, +) *ActivityCommon { + activity := &ActivityCommon{ + activityRepo: activityRepo, + activityQueueService: activityQueueService, + } + activity.activityQueueService.RegisterHandler(activity.HandleActivity) + return activity +} + +// HandleActivity handle activity message +func (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error { + activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey)) + if err != nil { + log.Errorf("error getting activity type %s, activity type is %d", err, activityType) + return err + } + + act := &entity.Activity{ + UserID: msg.UserID, + TriggerUserID: msg.TriggerUserID, + ObjectID: uid.DeShortID(msg.ObjectID), + OriginalObjectID: uid.DeShortID(msg.OriginalObjectID), + ActivityType: activityType, + Cancelled: entity.ActivityAvailable, + } + if len(msg.RevisionID) > 0 { + act.RevisionID = converter.StringToInt64(msg.RevisionID) + } + if err := ac.activityRepo.AddActivity(ctx, act); err != nil { + return err + } + return nil } diff --git a/internal/service/activity_common/follow.go b/internal/service/activity_common/follow.go index baeff38c1..6b0314732 100644 --- a/internal/service/activity_common/follow.go +++ b/internal/service/activity_common/follow.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity_common import "context" @@ -6,5 +25,6 @@ type FollowRepo interface { GetFollowIDs(ctx context.Context, userID, objectType string) (followIDs []string, err error) GetFollowAmount(ctx context.Context, objectID string) (followAmount int, err error) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) - IsFollowed(userId, objectId string) (bool, error) + IsFollowed(ctx context.Context, userId, objectId string) (bool, error) + MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error } diff --git a/internal/service/activity_common/vote.go b/internal/service/activity_common/vote.go index fb97885b2..af20f7f8d 100644 --- a/internal/service/activity_common/vote.go +++ b/internal/service/activity_common/vote.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package activity_common import ( @@ -7,4 +26,5 @@ import ( // VoteRepo activity repository type VoteRepo interface { GetVoteStatus(ctx context.Context, objectId, userId string) (status string) + GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) } diff --git a/internal/service/activity_queue/activity_queue.go b/internal/service/activity_queue/activity_queue.go new file mode 100644 index 000000000..7b8c1e3b8 --- /dev/null +++ b/internal/service/activity_queue/activity_queue.go @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package activity_queue + +import ( + "context" + + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" +) + +type ActivityQueueService interface { + Send(ctx context.Context, msg *schema.ActivityMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.ActivityMsg) error) +} + +type activityQueueService struct { + Queue chan *schema.ActivityMsg + Handler func(ctx context.Context, msg *schema.ActivityMsg) error +} + +func (ns *activityQueueService) Send(ctx context.Context, msg *schema.ActivityMsg) { + ns.Queue <- msg +} + +func (ns *activityQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.ActivityMsg) error) { + ns.Handler = handler +} + +func (ns *activityQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received activity %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for activity") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewActivityQueueService create a new activity queue service +func NewActivityQueueService() ActivityQueueService { + ns := &activityQueueService{} + ns.Queue = make(chan *schema.ActivityMsg, 128) + ns.working() + return ns +} diff --git a/internal/service/activity_type/activity_type.go b/internal/service/activity_type/activity_type.go index dde038907..5db98041d 100644 --- a/internal/service/activity_type/activity_type.go +++ b/internal/service/activity_type/activity_type.go @@ -1,32 +1,76 @@ -package activity_type +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ -import "github.com/answerdev/answer/internal/repo/config" +package activity_type const ( - QuestionVoteUp = "question.vote_up" - QuestionVoteDown = "question.vote_down" - AnswerVoteUp = "answer.vote_up" - AnswerVoteDown = "answer.vote_down" - CommentVoteUp = "comment.vote_up" - CommentVoteDown = "comment.vote_down" + QuestionVoteUp = "question.vote_up" + QuestionVoteDown = "question.vote_down" + QuestionVotedUp = "question.voted_up" + QuestionVotedDown = "question.voted_down" + AnswerVoteUp = "answer.vote_up" + AnswerVoteDown = "answer.vote_down" + AnswerVotedUp = "answer.voted_up" + AnswerVotedDown = "answer.voted_down" + AnswerAccepted = "answer.accepted" + AnswerAccept = "answer.accept" + CommentVoteUp = "comment.vote_up" + EditAccepted = "edit.accepted" ) var ( - activityTypeFlagMapping = map[string]string{ - QuestionVoteUp: "upvote", - QuestionVoteDown: "downvote", - AnswerVoteUp: "upvote", - AnswerVoteDown: "downvote", - CommentVoteUp: "upvote", - CommentVoteDown: "downvote", + ActivityTypeList = []string{ + QuestionVoteUp, + QuestionVoteDown, + QuestionVotedUp, + QuestionVotedDown, + AnswerVoteUp, + AnswerVoteDown, + AnswerVotedUp, + AnswerVotedDown, + AnswerAccepted, + AnswerAccept, + CommentVoteUp, } -) - -func Format(activityTypeID int) string { - activityTypeStr := config.ID2KeyMapping[activityTypeID] - activityTypeFlag := activityTypeFlagMapping[activityTypeStr] - if len(activityTypeFlag) == 0 { - return "edit" // to edit + VoteActivityTypeList = []string{ + QuestionVoteUp, + QuestionVoteDown, + QuestionVotedUp, + QuestionVotedDown, + AnswerVoteUp, + AnswerVoteDown, + AnswerVotedUp, + AnswerVotedDown, + CommentVoteUp, + } + ActivityTypeFlagMapping = map[string]string{ + QuestionVoteUp: "action_activity_type.upvote", + QuestionVoteDown: "action_activity_type.downvote", + QuestionVotedUp: "action_activity_type.upvoted", + QuestionVotedDown: "action_activity_type.downvoted", + AnswerVoteUp: "action_activity_type.upvote", + AnswerVoteDown: "action_activity_type.downvote", + AnswerVotedUp: "action_activity_type.upvoted", + AnswerVotedDown: "action_activity_type.downvoted", + AnswerAccepted: "action_activity_type.accepted", + AnswerAccept: "action_activity_type.accept", + CommentVoteUp: "action_activity_type.upvote", + EditAccepted: "action_activity_type.edit", } - return activityTypeFlag // todo i18n support -} +) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 7fc65a6f4..989eb143d 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -1,25 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package answercommon import ( "context" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/uid" ) type AnswerRepo interface { AddAnswer(ctx context.Context, answer *entity.Answer) (err error) RemoveAnswer(ctx context.Context, id string) (err error) - UpdateAnswer(ctx context.Context, answer *entity.Answer, Colar []string) (err error) + RecoverAnswer(ctx context.Context, answerID string) (err error) + UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) GetAnswer(ctx context.Context, id string) (answer *entity.Answer, exist bool, err error) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) - UpdateAdopted(ctx context.Context, id string, questionId string) error - GetByID(ctx context.Context, id string) (*entity.Answer, bool, error) - GetByUserIdQuestionId(ctx context.Context, userId string, questionId string) (*entity.Answer, bool, error) + UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error + GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) + GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) + GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) + GetCountByUserID(ctx context.Context, userID string) (int64, error) + GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) - CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) - UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) + GetPersonalAnswerPage(ctx context.Context, cond *entity.PersonalAnswerPageQueryCond) ( + resp []*entity.Answer, total int64, err error) + AdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error) + UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) + GetAnswerCount(ctx context.Context) (count int64, err error) + RemoveAllUserAnswer(ctx context.Context, userID string) (err error) + SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) + DeletePermanentlyAnswers(ctx context.Context) (err error) } // AnswerCommon user service @@ -33,16 +65,24 @@ func NewAnswerCommon(answerRepo AnswerRepo) *AnswerCommon { } } -func (as *AnswerCommon) SearchAnswered(ctx context.Context, userId, questionId string) (bool, error) { - _, has, err := as.answerRepo.GetByUserIdQuestionId(ctx, userId, questionId) +func (as *AnswerCommon) SearchAnswerIDs(ctx context.Context, userID, questionID string) ([]string, error) { + ids, err := as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userID, questionID) if err != nil { - return has, err + return nil, err } - return has, nil + return ids, nil } -func (as *AnswerCommon) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) { - return as.answerRepo.CmsSearchList(ctx, search) +func (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp []*entity.Answer, count int64, err error) { + resp, count, err = as.answerRepo.AdminSearchList(ctx, req) + if handler.GetEnableShortID(ctx) { + for _, item := range resp { + item.ID = uid.EnShortID(item.ID) + item.QuestionID = uid.EnShortID(item.QuestionID) + } + } + return resp, count, err } func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) { @@ -53,29 +93,44 @@ func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) return list, count, err } +func (as *AnswerCommon) PersonalAnswerPage(ctx context.Context, + cond *entity.PersonalAnswerPageQueryCond) ([]*entity.Answer, int64, error) { + return as.answerRepo.GetPersonalAnswerPage(ctx, cond) +} + func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { info := schema.AnswerInfo{} info.ID = data.ID - info.QuestionId = data.QuestionID + info.QuestionID = data.QuestionID info.Content = data.OriginalText - info.Html = data.ParsedText - info.Adopted = data.Adopted + info.HTML = data.ParsedText + info.Accepted = data.Accepted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() - info.UserId = data.UserID + if data.UpdatedAt.Unix() < 1 { + info.UpdateTime = 0 + } + info.UserID = data.UserID + info.UpdateUserID = data.LastEditUserID + info.Status = data.Status + info.MemberActions = make([]*schema.PermissionMemberAction, 0) return &info } func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer) *schema.AdminAnswerInfo { info := schema.AdminAnswerInfo{} info.ID = data.ID - info.QuestionId = data.QuestionID - info.Description = data.ParsedText - info.Adopted = data.Adopted + info.QuestionID = data.QuestionID + info.Accepted = data.Accepted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() - info.UserId = data.UserID + if data.UpdatedAt.Unix() < 1 { + info.UpdateTime = 0 + } + info.UserID = data.UserID + info.UpdateUserID = data.LastEditUserID + info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go deleted file mode 100644 index 11e729644..000000000 --- a/internal/service/answer_service.go +++ /dev/null @@ -1,441 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/service/activity" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/notice_queue" - "github.com/answerdev/answer/internal/service/revision_common" - - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - "github.com/answerdev/answer/internal/service/permission" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" -) - -// AnswerService user service -type AnswerService struct { - answerRepo answercommon.AnswerRepo - questionRepo questioncommon.QuestionRepo - questionCommon *questioncommon.QuestionCommon - answerActivityService *activity.AnswerActivityService - userCommon *usercommon.UserCommon - collectionCommon *collectioncommon.CollectionCommon - userRepo usercommon.UserRepo - revisionService *revision_common.RevisionService - AnswerCommon *answercommon.AnswerCommon - voteRepo activity_common.VoteRepo -} - -func NewAnswerService( - answerRepo answercommon.AnswerRepo, - questionRepo questioncommon.QuestionRepo, - questionCommon *questioncommon.QuestionCommon, - userCommon *usercommon.UserCommon, - collectionCommon *collectioncommon.CollectionCommon, - userRepo usercommon.UserRepo, - revisionService *revision_common.RevisionService, - answerAcceptActivityRepo *activity.AnswerActivityService, - answerCommon *answercommon.AnswerCommon, - voteRepo activity_common.VoteRepo, -) *AnswerService { - return &AnswerService{ - answerRepo: answerRepo, - questionRepo: questionRepo, - userCommon: userCommon, - collectionCommon: collectionCommon, - questionCommon: questionCommon, - userRepo: userRepo, - revisionService: revisionService, - answerActivityService: answerAcceptActivityRepo, - AnswerCommon: answerCommon, - voteRepo: voteRepo, - } -} - -// RemoveAnswer delete answer -func (as *AnswerService) RemoveAnswer(ctx context.Context, id string) (err error) { - answerInfo, exist, err := as.answerRepo.GetByID(ctx, id) - if err != nil { - return err - } - if !exist { - return nil - } - - //user add question count - err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID, -1) - if err != nil { - log.Error("IncreaseAnswerCount error", err.Error()) - } - - err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, -1) - if err != nil { - log.Error("user IncreaseAnswerCount error", err.Error()) - } - - err = as.answerRepo.RemoveAnswer(ctx, id) - if err != nil { - return err - } - err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) - if err != nil { - log.Errorf("delete answer activity change failed: %s", err.Error()) - } - return -} - -func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) { - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionId) - if err != nil { - return "", err - } - if !exist { - return "", errors.BadRequest(reason.QuestionNotFound) - } - now := time.Now() - insertData := new(entity.Answer) - insertData.UserID = req.UserID - insertData.OriginalText = req.Content - insertData.ParsedText = req.Html - insertData.Adopted = schema.Answer_Adopted_Failed - insertData.QuestionID = req.QuestionId - insertData.RevisionID = "0" - insertData.Status = entity.AnswerStatusAvailable - insertData.UpdatedAt = now - if err := as.answerRepo.AddAnswer(ctx, insertData); err != nil { - return "", err - } - err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionId, 1) - if err != nil { - log.Error("IncreaseAnswerCount error", err.Error()) - } - err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionId, insertData.ID) - if err != nil { - log.Error("UpdateLastAnswer error", err.Error()) - } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionId) - if err != nil { - return insertData.ID, err - } - - err = as.userCommon.UpdateAnswerCount(ctx, req.UserID, 1) - if err != nil { - log.Error("user IncreaseAnswerCount error", err.Error()) - } - - revisionDTO := &schema.AddRevisionDTO{ - UserID: insertData.UserID, - ObjectID: insertData.ID, - Title: "", - } - InfoJson, _ := json.Marshal(insertData) - revisionDTO.Content = string(InfoJson) - err = as.revisionService.AddRevision(ctx, revisionDTO, true) - if err != nil { - return insertData.ID, err - } - as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, insertData.ID, req.UserID) - return insertData.ID, nil -} - -func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) { - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionId) - if err != nil { - return "", err - } - if !exist { - return "", errors.BadRequest(reason.QuestionNotFound) - } - now := time.Now() - insertData := new(entity.Answer) - insertData.ID = req.ID - insertData.QuestionID = req.QuestionId - insertData.UserID = req.UserID - insertData.OriginalText = req.Content - insertData.ParsedText = req.Html - insertData.UpdatedAt = now - if err := as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil { - return "", err - } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionId) - if err != nil { - return insertData.ID, err - } - revisionDTO := &schema.AddRevisionDTO{ - UserID: req.UserID, - ObjectID: req.ID, - Title: "", - Log: req.EditSummary, - } - InfoJson, _ := json.Marshal(insertData) - revisionDTO.Content = string(InfoJson) - err = as.revisionService.AddRevision(ctx, revisionDTO, true) - if err != nil { - return insertData.ID, err - } - as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID) - return insertData.ID, nil -} - -// UpdateAdopted -func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAdoptedReq) error { - if req.AnswerID == "" { - req.AnswerID = "0" - } - if req.UserID == "" { - return nil - } - - newAnswerInfo := &entity.Answer{} - newAnswerInfoexist := false - var err error - - if req.AnswerID != "0" { - newAnswerInfo, newAnswerInfoexist, err = as.answerRepo.GetByID(ctx, req.AnswerID) - if err != nil { - return err - } - if !newAnswerInfoexist { - return errors.BadRequest(reason.AnswerNotFound) - } - } - - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) - if err != nil { - return err - } - if !exist { - return errors.BadRequest(reason.QuestionNotFound) - } - if questionInfo.UserID != req.UserID { - return fmt.Errorf("no permission to set answer") - } - if questionInfo.AcceptedAnswerID == req.AnswerID { - return nil - } - - var oldAnswerInfo *entity.Answer - if len(questionInfo.AcceptedAnswerID) > 0 && questionInfo.AcceptedAnswerID != "0" { - oldAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID) - if err != nil { - return err - } - } - - err = as.answerRepo.UpdateAdopted(ctx, req.AnswerID, req.QuestionID) - if err != nil { - return err - } - - err = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID) - if err != nil { - log.Error("UpdateLastAnswer error", err.Error()) - } - - as.updateAnswerRank(ctx, req.UserID, questionInfo, newAnswerInfo, oldAnswerInfo) - return nil -} - -func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, - questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer) { - - // if this question is already been answered, should cancel old answer rank - if oldAnswerInfo != nil { - err := as.answerActivityService.CancelAcceptAnswer( - ctx, questionInfo.AcceptedAnswerID, questionInfo.UserID, oldAnswerInfo.UserID) - if err != nil { - log.Error(err) - } - } - if newAnswerInfo.ID != "" { - err := as.answerActivityService.AcceptAnswer( - ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID) - if err != nil { - log.Error(err) - } - } - -} - -func (as *AnswerService) Get(ctx context.Context, answerID, loginUserId string) (*schema.AnswerInfo, *schema.QuestionInfo, bool, error) { - answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) - if err != nil { - return nil, nil, has, err - } - info := as.ShowFormat(ctx, answerInfo) - //todo questionFunc - questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserId) - if err != nil { - return nil, nil, has, err - } - //todo UserFunc - userinfo, has, err := as.userCommon.GetUserBasicInfoByID(ctx, answerInfo.UserID) - if err != nil { - return nil, nil, has, err - } - if has { - info.UserInfo = userinfo - info.UpdateUserInfo = userinfo - } - - if loginUserId == "" { - return info, questionInfo, has, nil - } - - info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserId) - - CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserId, []string{answerInfo.ID}) - if err != nil { - log.Error("CollectionFunc.SearchObjectCollected error", err) - } - _, ok := CollectedMap[answerInfo.ID] - if ok { - info.Collected = true - } - - return info, questionInfo, has, nil -} - -func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID string, setStatusStr string) error { - setStatus, ok := entity.CmsAnswerSearchStatus[setStatusStr] - if !ok { - return fmt.Errorf("question status does not exist") - } - answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, answerID) - if err != nil { - return err - } - if !exist { - return fmt.Errorf("answer does not exist") - } - answerInfo.Status = setStatus - err = as.answerRepo.UpdateAnswerStatus(ctx, answerInfo) - if err != nil { - return err - } - - if setStatus == entity.AnswerStatusDeleted { - err = as.answerActivityService.DeleteQuestion(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) - if err != nil { - log.Errorf("admin delete question then rank rollback error %s", err.Error()) - } - } - - msg := &schema.NotificationMsg{} - msg.ObjectID = answerInfo.ID - msg.Type = schema.NotificationTypeInbox - msg.ReceiverUserID = answerInfo.UserID - msg.TriggerUserID = answerInfo.UserID - msg.ObjectType = constant.AnswerObjectType - msg.NotificationAction = constant.YourAnswerWasDeleted - notice_queue.AddNotification(msg) - - return nil -} - -func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerList) ([]*schema.AnswerInfo, int64, error) { - list := make([]*schema.AnswerInfo, 0) - dbSearch := entity.AnswerSearch{} - dbSearch.QuestionID = search.QuestionId - dbSearch.Page = search.Page - dbSearch.PageSize = search.PageSize - dbSearch.Order = search.Order - dblist, count, err := as.answerRepo.SearchList(ctx, &dbSearch) - if err != nil { - return list, count, err - } - AnswerList, err := as.SearchFormatInfo(ctx, dblist, search.LoginUserID) - if err != nil { - return AnswerList, count, err - } - return AnswerList, count, nil -} - -func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.Answer, loginUserId string) ([]*schema.AnswerInfo, error) { - list := make([]*schema.AnswerInfo, 0) - objectIds := make([]string, 0) - userIds := make([]string, 0) - for _, dbitem := range dblist { - item := as.ShowFormat(ctx, dbitem) - list = append(list, item) - objectIds = append(objectIds, dbitem.ID) - userIds = append(userIds, dbitem.UserID) - if loginUserId != "" { - //item.VoteStatus = as.activityFunc.GetVoteStatus(ctx, item.TagID, loginUserId) - item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, loginUserId) - } - } - userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds) - if err != nil { - return list, err - } - for _, item := range list { - _, ok := userInfoMap[item.UserId] - if ok { - item.UserInfo = userInfoMap[item.UserId] - item.UpdateUserInfo = userInfoMap[item.UserId] - } - } - - if loginUserId == "" { - return list, nil - } - - CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserId, objectIds) - if err != nil { - log.Error("CollectionFunc.SearchObjectCollected error", err) - } - - for _, item := range list { - _, ok := CollectedMap[item.ID] - if ok { - item.Collected = true - } - } - - for _, item := range list { - item.MemberActions = permission.GetAnswerPermission(loginUserId, item.UserId) - } - - return list, nil -} - -func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { - return as.AnswerCommon.ShowFormat(ctx, data) -} - -func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) { - msg := &schema.NotificationMsg{ - TriggerUserID: answerUserID, - ReceiverUserID: questionUserID, - Type: schema.NotificationTypeInbox, - ObjectID: answerID, - } - msg.ObjectType = constant.AnswerObjectType - msg.NotificationAction = constant.UpdateAnswer - notice_queue.AddNotification(msg) -} - -func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, questionUserID, answerID, answerUserID string) { - msg := &schema.NotificationMsg{ - TriggerUserID: answerUserID, - ReceiverUserID: questionUserID, - Type: schema.NotificationTypeInbox, - ObjectID: answerID, - } - msg.ObjectType = constant.AnswerObjectType - msg.NotificationAction = constant.AnswerTheQuestion - notice_queue.AddNotification(msg) -} diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index c7f4e7125..9ee5f3ee0 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -1,23 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package auth import ( "context" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/pkg/token" - "github.com/segmentfault/pacman/log" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/plugin" ) // AuthRepo auth repository type AuthRepo interface { GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) - SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error + SetUserCacheInfo(ctx context.Context, accessToken, visitToken string, userInfo *entity.UserCacheInfo) error + GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) + RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) + SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) RemoveUserStatus(ctx context.Context, userID string) (err error) - GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) - SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error - RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) + GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) + SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error + RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) + AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) + RemoveUserTokens(ctx context.Context, userID string, remainToken string) } // AuthService kit service @@ -37,52 +61,94 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string) if err != nil { return nil, err } + if userCacheInfo == nil { + return nil, nil + } cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID) if cacheInfo != nil { - log.Infof("user status updated: %+v", cacheInfo) userCacheInfo.UserStatus = cacheInfo.UserStatus userCacheInfo.EmailStatus = cacheInfo.EmailStatus + userCacheInfo.RoleID = cacheInfo.RoleID // update current user cache info - err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo) + err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo.VisitToken, userCacheInfo) if err != nil { return nil, err } } + + // try to get user status from user center + uc, ok := plugin.GetUserCenter() + if ok && len(userCacheInfo.ExternalID) > 0 { + if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable { + userCacheInfo.UserStatus = int(userStatus) + } + } return userCacheInfo, nil } -func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.UserCacheInfo) (accessToken string, err error) { +func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.UserCacheInfo) ( + accessToken string, visitToken string, err error) { accessToken = token.GenerateToken() - err = as.authRepo.SetUserCacheInfo(ctx, accessToken, userInfo) - return accessToken, err + visitToken = token.GenerateToken() + err = as.authRepo.SetUserCacheInfo(ctx, accessToken, visitToken, userInfo) + if err != nil { + return "", "", err + } + return accessToken, visitToken, err } -func (as *AuthService) UpdateUserCacheInfo(ctx context.Context, token string, userInfo *entity.UserCacheInfo) (err error) { - err = as.authRepo.SetUserCacheInfo(ctx, token, userInfo) +func (as *AuthService) CheckUserVisitToken(ctx context.Context, visitToken string) bool { + accessToken, err := as.authRepo.GetUserVisitCacheInfo(ctx, visitToken) if err != nil { - return err + return false } - if err := as.authRepo.RemoveUserStatus(ctx, userInfo.UserID); err != nil { - log.Error(err) + if len(accessToken) == 0 { + return false } - return + return true +} + +func (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) { + return as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo) } func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { return as.authRepo.RemoveUserCacheInfo(ctx, accessToken) } -//cms +func (as *AuthService) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) { + if len(visitToken) > 0 { + return as.authRepo.RemoveUserVisitCacheInfo(ctx, visitToken) + } + return nil +} + +// AddUserTokenMapping add user token mapping +func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) { + return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken) +} + +// RemoveUserAllTokens Log out all users under this user id +func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) { + as.authRepo.RemoveUserTokens(ctx, userID, "") +} + +// RemoveTokensExceptCurrentUser remove all tokens except the current user +func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) { + as.authRepo.RemoveUserTokens(ctx, userID, accessToken) +} + +//Admin -func (as *AuthService) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { - return as.authRepo.GetCmsUserCacheInfo(ctx, accessToken) +func (as *AuthService) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { + return as.authRepo.GetAdminUserCacheInfo(ctx, accessToken) } -func (as *AuthService) SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { - err = as.authRepo.SetCmsUserCacheInfo(ctx, accessToken, userInfo) +func (as *AuthService) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { + err = as.authRepo.SetAdminUserCacheInfo(ctx, accessToken, userInfo) return err } -func (as *AuthService) RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) { - return as.authRepo.RemoveCmsUserCacheInfo(ctx, accessToken) +func (as *AuthService) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) { + return as.authRepo.RemoveAdminUserCacheInfo(ctx, accessToken) } diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go new file mode 100644 index 000000000..397a7471a --- /dev/null +++ b/internal/service/badge/badge_award_service.go @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/object_info" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/uid" + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +type BadgeAwardRepo interface { + CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool, err error) + AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) + + CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) + CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) + + SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) + + ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) + ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) + + GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) + GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) + + DeleteUserBadgeAward(ctx context.Context, userID string) (err error) +} + +type BadgeAwardService struct { + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + notificationQueueService notice_queue.NotificationQueueService +} + +func NewBadgeAwardService( + badgeAwardRepo BadgeAwardRepo, + badgeRepo BadgeRepo, + userCommon *usercommon.UserCommon, + objectInfoService *object_info.ObjService, + notificationQueueService notice_queue.NotificationQueueService, +) *BadgeAwardService { + return &BadgeAwardService{ + badgeAwardRepo: badgeAwardRepo, + badgeRepo: badgeRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + notificationQueueService: notificationQueueService, + } +} + +// GetBadgeAwardList get badge award list +func (bs *BadgeAwardService) GetBadgeAwardList( + ctx context.Context, + req *schema.GetBadgeAwardWithPageReq, +) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { + var ( + badgeAwardList []*entity.BadgeAward + ) + + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) + if err != nil { + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + } else { + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) + } + + if err != nil { + return + } + + resp = make([]*schema.GetBadgeAwardWithPageResp, len(badgeAwardList)) + + for i, badgeAward := range badgeAwardList { + var ( + objectID, questionID, answerID, commentID, objectType, urlTitle string + ) + + // if exist object info + objInfo, e := bs.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) + if e == nil && !objInfo.IsDeleted() { + objectID = objInfo.ObjectID + questionID = objInfo.QuestionID + answerID = objInfo.AnswerID + commentID = objInfo.CommentID + objectType = objInfo.ObjectType + urlTitle = objInfo.Title + } + + row := &schema.GetBadgeAwardWithPageResp{ + CreatedAt: badgeAward.CreatedAt.Unix(), + ObjectID: objectID, + QuestionID: questionID, + AnswerID: answerID, + CommentID: commentID, + ObjectType: objectType, + UrlTitle: urlTitle, + AuthorUserInfo: schema.UserBasicInfo{}, + } + + // get user info + userInfo, exists, e := bs.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", badgeAward.UserID, e) + } + if exists { + _ = copier.Copy(&row.AuthorUserInfo, userInfo) + } + + resp[i] = row + } + + return +} + +// Award award badge +func (bs *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { + badgeData, exists, err := bs.badgeRepo.GetByID(ctx, badgeID) + if err != nil { + return err + } + + if !exists || badgeData.Status == entity.BadgeStatusInactive { + return errors.BadRequest(reason.BadgeObjectNotFound) + } + + alreadyAwarded, err := bs.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) + if err != nil { + return err + } + if alreadyAwarded { + return nil + } + + badgeAward := &entity.BadgeAward{ + UserID: userID, + BadgeID: badgeID, + AwardKey: awardKey, + BadgeGroupID: badgeData.BadgeGroupID, + IsBadgeDeleted: entity.IsBadgeNotDeleted, + } + err = bs.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) + if err != nil { + return err + } + + msg := &schema.NotificationMsg{ + TriggerUserID: badgeAward.UserID, + ReceiverUserID: badgeAward.UserID, + Type: schema.NotificationTypeAchievement, + ObjectID: badgeAward.ID, + ObjectType: constant.BadgeAwardObjectType, + Title: badgeData.Name, + ExtraInfo: map[string]string{"badge_id": badgeData.ID}, + NotificationAction: constant.NotificationEarnedBadge, + } + bs.notificationQueueService.Send(ctx, msg) + return nil +} + +// GetUserBadgeAwardList get user badge award list +func (bs *BadgeAwardService) GetUserBadgeAwardList( + ctx *gin.Context, + req *schema.GetUserBadgeAwardListReq, +) ( + resp []*schema.GetUserBadgeAwardListResp, + total int64, + err error, +) { + var ( + earnedCounts []*entity.BadgeEarnedCount + ) + + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) + if err != nil { + return + } + + earnedCounts, err = bs.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + if err != nil { + return + } + total = int64(len(earnedCounts)) + resp = make([]*schema.GetUserBadgeAwardListResp, total) + + for i, earnedCount := range earnedCounts { + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + if e != nil { + err = e + return + } + if !exists { + continue + } + resp[i] = &schema.GetUserBadgeAwardListResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + EarnedCount: earnedCount.EarnedCount, + Level: badge.Level, + } + } + + return +} + +// GetUserRecentBadgeAwardList get user badge award list +func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req *schema.GetUserBadgeAwardListReq) ( + resp []*schema.GetUserBadgeAwardListResp, total int64, err error) { + var ( + earnedCounts []*entity.BadgeAwardRecent + ) + + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) + if err != nil { + return + } + + earnedCounts, err = bs.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) + if err != nil { + return + } + + total = int64(len(earnedCounts)) + resp = make([]*schema.GetUserBadgeAwardListResp, total) + + for i, earnedCount := range earnedCounts { + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + if e != nil { + err = e + return + } + if !exists { + continue + } + resp[i] = &schema.GetUserBadgeAwardListResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + EarnedCount: earnedCount.EarnedCount, + Level: badge.Level, + } + } + + return +} + +func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { + var ( + userInfo *schema.UserBasicInfo + exist bool + ) + // validate user exists or not + if len(userName) > 0 { + userInfo, exist, err = bs.userCommon.GetUserBasicInfoByUserName(ctx, userName) + if err != nil { + return + } + if !exist { + err = errors.BadRequest(reason.UserNotFound) + return + } + userID = userInfo.ID + } + if len(userID) == 0 { + err = errors.BadRequest(reason.UserNotFound) + return + } + return +} diff --git a/internal/service/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go new file mode 100644 index 000000000..219822947 --- /dev/null +++ b/internal/service/badge/badge_event_handler.go @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/event_queue" + "github.com/segmentfault/pacman/log" +) + +type BadgeEventService struct { + data *data.Data + eventQueueService event_queue.EventQueueService + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + eventRuleRepo EventRuleRepo + badgeAwardService *BadgeAwardService +} + +type EventRuleHandler func(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) + +type EventRuleRepo interface { + HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) (awards []*entity.BadgeAward) +} + +func NewBadgeEventService( + data *data.Data, + eventQueueService event_queue.EventQueueService, + badgeRepo BadgeRepo, + eventRuleRepo EventRuleRepo, + badgeAwardService *BadgeAwardService, +) *BadgeEventService { + n := &BadgeEventService{ + data: data, + eventQueueService: eventQueueService, + badgeRepo: badgeRepo, + eventRuleRepo: eventRuleRepo, + badgeAwardService: badgeAwardService, + } + eventQueueService.RegisterHandler(n.Handler) + return n +} + +func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error { + awards := ns.eventRuleRepo.HandleEventWithRule(ctx, msg) + if len(awards) == 0 { + return nil + } + + for _, award := range awards { + err := ns.badgeAwardService.Award(ctx, award.BadgeID, award.UserID, award.AwardKey) + if err != nil { + log.Debugf("error awarding badge %s: %v", award.BadgeID, err) + } + } + return nil +} diff --git a/internal/service/badge/badge_group_service.go b/internal/service/badge/badge_group_service.go new file mode 100644 index 000000000..e0dab6e89 --- /dev/null +++ b/internal/service/badge/badge_group_service.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/entity" +) + +type BadgeGroupRepo interface { + ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) + AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) +} + +type BadgeGroupService struct { + badgeGroupRepo BadgeGroupRepo +} + +func NewBadgeGroupService(badgeGroupRepo BadgeGroupRepo) *BadgeGroupService { + return &BadgeGroupService{ + badgeGroupRepo: badgeGroupRepo, + } +} diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go new file mode 100644 index 000000000..7bc9ffe21 --- /dev/null +++ b/internal/service/badge/badge_service.go @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/uid" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "strings" +) + +type BadgeRepo interface { + GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) + GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) + + ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) + ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) + ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) + + UpdateStatus(ctx context.Context, id string, status int8) (err error) + UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) +} + +type BadgeService struct { + badgeRepo BadgeRepo + badgeGroupRepo BadgeGroupRepo + badgeAwardRepo BadgeAwardRepo + badgeEventService *BadgeEventService + siteInfoCommonService siteinfo_common.SiteInfoCommonService +} + +func NewBadgeService( + badgeRepo BadgeRepo, + badgeGroupRepo BadgeGroupRepo, + badgeAwardRepo BadgeAwardRepo, + badgeEventService *BadgeEventService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, +) *BadgeService { + return &BadgeService{ + badgeRepo: badgeRepo, + badgeGroupRepo: badgeGroupRepo, + badgeAwardRepo: badgeAwardRepo, + badgeEventService: badgeEventService, + siteInfoCommonService: siteInfoCommonService, + } +} + +// ListByGroup list all badges group by group +func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) { + var ( + groups []*entity.BadgeGroup + badges []*entity.Badge + earnedCounts []*entity.BadgeEarnedCount + + groupMap = make(map[int64]string, 0) + badgesMap = make(map[int64][]*schema.BadgeListInfo, 0) + ) + resp = make([]*schema.GetBadgeListResp, 0) + + groups, err = b.badgeGroupRepo.ListGroups(ctx) + if err != nil { + return + } + badges, _, err = b.badgeRepo.ListActivated(ctx, 0, 0) + if err != nil { + return + } + + if len(userID) > 0 { + earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID) + if err != nil { + return + } + } + + for _, group := range groups { + groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) + } + + for _, badge := range badges { + // check is earned + var earned int64 = 0 + if len(earnedCounts) > 0 { + for _, earnedCount := range earnedCounts { + if badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 { + earned = earnedCount.EarnedCount + break + } + } + } + + badgesMap[badge.BadgeGroupID] = append(badgesMap[badge.BadgeGroupID], &schema.BadgeListInfo{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + EarnedCount: earned, + Level: badge.Level, + }) + } + + for _, group := range groups { + resp = append(resp, &schema.GetBadgeListResp{ + GroupName: translator.Tr(handler.GetLangByCtx(ctx), group.Name), + Badges: badgesMap[converter.StringToInt64(group.ID)], + }) + } + + return +} + +// ListPaged list all badges by page +func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPagedReq) (resp []*schema.GetBadgeListPagedResp, total int64, err error) { + var ( + groups []*entity.BadgeGroup + badges []*entity.Badge + badge *entity.Badge + exists bool + groupMap = make(map[int64]string, 0) + ) + + total = 0 + + if len(req.Query) > 0 { + isID := strings.Index(req.Query, "badge:") + if isID != 0 { + badges, err = b.searchByName(ctx, req.Query) + if err != nil { + return + } + // paged result + count := len(badges) + total = int64(count) + start := (req.Page - 1) * req.PageSize + end := req.Page * req.PageSize + if start >= count { + start = count + end = count + } + if end > count { + end = count + } + badges = badges[start:end] + } else { + req.Query = strings.TrimSpace(strings.TrimLeft(req.Query, "badge:")) + id := uid.DeShortID(req.Query) + if len(id) == 0 { + return + } + badge, exists, err = b.badgeRepo.GetByID(ctx, id) + if err != nil || !exists { + return + } + badges = append(badges, badge) + } + } else { + switch req.Status { + case schema.BadgeStatusActive: + badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize) + case schema.BadgeStatusInactive: + badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize) + default: + badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize) + } + if err != nil { + return + } + } + + // find all group and build group map + groups, err = b.badgeGroupRepo.ListGroups(ctx) + if err != nil { + return + } + for _, group := range groups { + groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) + } + + resp = make([]*schema.GetBadgeListPagedResp, len(badges)) + + general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx) + var baseURL = "" + if siteErr != nil { + baseURL = "" + } else { + baseURL = general.SiteUrl + } + + for i, badge := range badges { + resp[i] = &schema.GetBadgeListPagedResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + Level: badge.Level, + GroupName: groupMap[badge.BadgeGroupID], + Status: schema.BadgeStatusMap[badge.Status], + } + } + return +} + +// searchByName +func (b *BadgeService) searchByName(ctx context.Context, name string) (result []*entity.Badge, err error) { + var badges []*entity.Badge + name = strings.ToLower(name) + result = make([]*entity.Badge, 0) + + badges, _, err = b.badgeRepo.ListPaged(ctx, 0, 0) + for _, badge := range badges { + tn := strings.ToLower(translator.Tr(handler.GetLangByCtx(ctx), badge.Name)) + if strings.Contains(tn, name) { + result = append(result, badge) + } + } + return +} + +// GetBadgeInfo get badge info +func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) { + var ( + badge *entity.Badge + earnedTotal int64 = 0 + exists = false + ) + + badge, exists, err = b.badgeRepo.GetByID(ctx, id) + if err != nil { + return + } + + if !exists || badge.Status == entity.BadgeStatusInactive { + err = errors.BadRequest(reason.BadgeObjectNotFound) + return + } + + if len(userID) > 0 { + earnedTotal = b.badgeAwardRepo.CountByUserIdAndBadgeId(ctx, userID, badge.ID) + } + + general, siteErr := b.siteInfoCommonService.GetSiteGeneral(ctx) + var baseURL = "" + if siteErr != nil { + baseURL = "" + } else { + baseURL = general.SiteUrl + } + + info = &schema.GetBadgeInfoResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Description: translator.TrWithData(handler.GetLangByCtx(ctx), badge.Description, &schema.BadgeTplData{ProfileURL: baseURL + "/users/settings/profile"}), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + EarnedCount: earnedTotal, + IsSingle: badge.Single == entity.BadgeSingleAward, + Level: badge.Level, + } + return +} + +// UpdateStatus update badge status +func (b *BadgeService) UpdateStatus(ctx *gin.Context, req *schema.UpdateBadgeStatusReq) (err error) { + req.ID = uid.DeShortID(req.ID) + + badge, exists, err := b.badgeRepo.GetByID(ctx, req.ID) + if err != nil { + return err + } + if !exists { + return errors.BadRequest(reason.BadgeObjectNotFound) + } + + // check duplicate action + status, ok := schema.BadgeStatusEMap[req.Status] + if !ok { + err = errors.BadRequest(reason.StatusInvalid) + return + } + if badge.Status == status { + return + } + + err = b.badgeRepo.UpdateStatus(ctx, req.ID, status) + if err != nil { + return err + } + + if status == entity.BadgeStatusActive { + count, err := b.badgeAwardRepo.CountByBadgeID(ctx, badge.ID) + if err != nil { + log.Errorf("count badge award failed: %v", err) + return nil + } + err = b.badgeRepo.UpdateAwardCount(ctx, badge.ID, int(count)) + if err != nil { + log.Errorf("update badge award count failed: %v", err) + return nil + } + } + return nil +} diff --git a/internal/service/collection_group_service.go b/internal/service/collection/collection_group_service.go similarity index 66% rename from internal/service/collection_group_service.go rename to internal/service/collection/collection_group_service.go index 3c6a3a2be..244d84caa 100644 --- a/internal/service/collection_group_service.go +++ b/internal/service/collection/collection_group_service.go @@ -1,11 +1,30 @@ -package service +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package collection import ( "context" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" ) @@ -14,10 +33,11 @@ import ( type CollectionGroupRepo interface { AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) + CreateDefaultGroupIfNotExist(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) GetCollectionGroup(ctx context.Context, id string) (collectionGroup *entity.CollectionGroup, exist bool, err error) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) - GetDefaultID(ctx context.Context, userId string) (collectionGroup *entity.CollectionGroup, has bool, err error) + GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) } // CollectionGroupService user service diff --git a/internal/service/collection/collection_service.go b/internal/service/collection/collection_service.go new file mode 100644 index 000000000..6f02ccf9a --- /dev/null +++ b/internal/service/collection/collection_service.go @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package collection + +import ( + "context" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + questioncommon "github.com/apache/answer/internal/service/question_common" +) + +// CollectionService user service +type CollectionService struct { + collectionRepo collectioncommon.CollectionRepo + collectionGroupRepo CollectionGroupRepo + questionCommon *questioncommon.QuestionCommon +} + +func NewCollectionService( + collectionRepo collectioncommon.CollectionRepo, + collectionGroupRepo CollectionGroupRepo, + questionCommon *questioncommon.QuestionCommon, +) *CollectionService { + return &CollectionService{ + collectionRepo: collectionRepo, + collectionGroupRepo: collectionGroupRepo, + questionCommon: questionCommon, + } +} + +func (cs *CollectionService) CollectionSwitch(ctx context.Context, req *schema.CollectionSwitchReq) ( + resp *schema.CollectionSwitchResp, err error) { + collectionGroup, err := cs.collectionGroupRepo.CreateDefaultGroupIfNotExist(ctx, req.UserID) + if err != nil { + return nil, err + } + + collection, exist, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, req.UserID, req.ObjectID) + if err != nil { + return nil, err + } + if (!req.Bookmark && !exist) || (req.Bookmark && exist) { + return nil, nil + } + + if req.Bookmark { + collection = &entity.Collection{ + UserID: req.UserID, + ObjectID: req.ObjectID, + UserCollectionGroupID: collectionGroup.ID, + } + err = cs.collectionRepo.AddCollection(ctx, collection) + } else { + err = cs.collectionRepo.RemoveCollection(ctx, collection.ID) + } + if err != nil { + return nil, err + } + + // For now, we only support bookmark for question, so we just update question collection count + resp = &schema.CollectionSwitchResp{} + resp.ObjectCollectionCount, err = cs.questionCommon.UpdateCollectionCount(ctx, req.ObjectID) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/service/collection_common/collection.go b/internal/service/collection_common/collection.go index 42f113419..1d8f05d05 100644 --- a/internal/service/collection_common/collection.go +++ b/internal/service/collection_common/collection.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package collectioncommon import ( "context" - "github.com/answerdev/answer/internal/entity" + "github.com/apache/answer/internal/entity" ) // CollectionRepo collection repository diff --git a/internal/service/collection_service.go b/internal/service/collection_service.go deleted file mode 100644 index d22342894..000000000 --- a/internal/service/collection_service.go +++ /dev/null @@ -1,147 +0,0 @@ -package service - -import ( - "context" - "fmt" - - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" -) - -// CollectionService user service -type CollectionService struct { - collectionRepo collectioncommon.CollectionRepo - collectionGroupRepo CollectionGroupRepo - questionCommon *questioncommon.QuestionCommon -} - -func NewCollectionService( - collectionRepo collectioncommon.CollectionRepo, - collectionGroupRepo CollectionGroupRepo, - questionCommon *questioncommon.QuestionCommon, - -) *CollectionService { - return &CollectionService{ - collectionRepo: collectionRepo, - collectionGroupRepo: collectionGroupRepo, - questionCommon: questionCommon, - } -} -func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.CollectionSwitchDTO) (resp *schema.CollectionSwitchResp, err error) { - resp = &schema.CollectionSwitchResp{} - dbData, has, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, dto.UserID, dto.ObjectID) - if err != nil { - return - } - if has { - err = cs.collectionRepo.RemoveCollection(ctx, dbData.ID) - if err != nil { - return nil, err - } - err = cs.questionCommon.UpdateCollectionCount(ctx, dto.ObjectID, -1) - if err != nil { - log.Error("UpdateCollectionCount", err.Error()) - } - count, err := cs.objectCollectionCount(ctx, dto.ObjectID) - if err != nil { - return resp, err - } - resp.ObjectCollectionCount = fmt.Sprintf("%v", count) - resp.Switch = false - return resp, err - } - - if dto.GroupID == "" || dto.GroupID == "0" { - defaultGroup, has, err := cs.collectionGroupRepo.GetDefaultID(ctx, dto.UserID) - if err != nil { - return nil, err - } - if !has { - dbdefaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, dto.UserID) - if err != nil { - return nil, err - } - dto.GroupID = dbdefaultGroup.ID - } else { - dto.GroupID = defaultGroup.ID - } - } - collection := &entity.Collection{ - UserCollectionGroupID: dto.GroupID, - UserID: dto.UserID, - ObjectID: dto.ObjectID, - } - - err = cs.collectionRepo.AddCollection(ctx, collection) - if err != nil { - return - } - err = cs.questionCommon.UpdateCollectionCount(ctx, dto.ObjectID, 1) - if err != nil { - log.Error("UpdateCollectionCount", err.Error()) - } - count, err := cs.objectCollectionCount(ctx, dto.ObjectID) - if err != nil { - return - } - resp.ObjectCollectionCount = fmt.Sprintf("%d", count) - resp.Switch = true - return -} - -func (cs *CollectionService) objectCollectionCount(ctx context.Context, objectId string) (int64, error) { - count, err := cs.collectionRepo.CountByObjectID(ctx, objectId) - return count, err -} - -func (cs *CollectionService) add(ctx context.Context, collection *entity.Collection) error { - _, has, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, collection.UserID, collection.ObjectID) - if err != nil { - return err - } - if has { - return errors.BadRequest("already collected") - } - - if collection.UserCollectionGroupID == "" || collection.UserCollectionGroupID == "0" { - defaultGroup, has, err := cs.collectionGroupRepo.GetDefaultID(ctx, collection.UserID) - if err != nil { - return err - } - if !has { - defaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, collection.UserID) - if err != nil { - return err - } - collection.UserCollectionGroupID = defaultGroup.ID - - } else { - collection.UserCollectionGroupID = defaultGroup.ID - } - } - err = cs.collectionRepo.AddCollection(ctx, collection) - if err != nil { - return err - } - return nil -} - -// Cancel -func (cs *CollectionService) cancel(ctx context.Context, collection *entity.Collection) error { - dbData, has, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, collection.UserID, collection.ObjectID) - if err != nil { - return err - } - if !has { - return errors.BadRequest("collected record does not exist") - } - err = cs.collectionRepo.RemoveCollection(ctx, dbData.ID) - if err != nil { - return err - } - return nil -} diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 749f0becc..ba9bbe1d3 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -1,19 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package comment import ( "context" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/comment_common" - "github.com/answerdev/answer/internal/service/notice_queue" - object_info "github.com/answerdev/answer/internal/service/object_info" - "github.com/answerdev/answer/internal/service/permission" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/event_queue" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/internal/service/permission" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -23,20 +49,12 @@ import ( type CommentRepo interface { AddComment(ctx context.Context, comment *entity.Comment) (err error) RemoveComment(ctx context.Context, commentID string) (err error) - UpdateComment(ctx context.Context, comment *entity.Comment) (err error) + UpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error) + GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentPage(ctx context.Context, commentQuery *CommentQuery) ( comments []*entity.Comment, total int64, err error) } -// CommentService user service -type CommentService struct { - commentRepo CommentRepo - commentCommonRepo comment_common.CommentCommonRepo - userCommon *usercommon.UserCommon - voteCommon activity_common.VoteRepo - objectInfoService *object_info.ObjService -} - type CommentQuery struct { pager.PageCond // object id @@ -57,19 +75,47 @@ func (c *CommentQuery) GetOrderBy() string { return "created_at ASC" } +// CommentService user service +type CommentService struct { + commentRepo CommentRepo + commentCommonRepo comment_common.CommentCommonRepo + userCommon *usercommon.UserCommon + voteCommon activity_common.VoteRepo + objectInfoService *object_info.ObjService + emailService *export.EmailService + userRepo usercommon.UserRepo + notificationQueueService notice_queue.NotificationQueueService + externalNotificationQueueService notice_queue.ExternalNotificationQueueService + activityQueueService activity_queue.ActivityQueueService + eventQueueService event_queue.EventQueueService +} + // NewCommentService new comment service func NewCommentService( commentRepo CommentRepo, commentCommonRepo comment_common.CommentCommonRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, - voteCommon activity_common.VoteRepo) *CommentService { + voteCommon activity_common.VoteRepo, + emailService *export.EmailService, + userRepo usercommon.UserRepo, + notificationQueueService notice_queue.NotificationQueueService, + externalNotificationQueueService notice_queue.ExternalNotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, + eventQueueService event_queue.EventQueueService, +) *CommentService { return &CommentService{ - commentRepo: commentRepo, - commentCommonRepo: commentCommonRepo, - userCommon: userCommon, - voteCommon: voteCommon, - objectInfoService: objectInfoService, + commentRepo: commentRepo, + commentCommonRepo: commentCommonRepo, + userCommon: userCommon, + voteCommon: voteCommon, + objectInfoService: objectInfoService, + emailService: emailService, + userRepo: userRepo, + notificationQueueService: notificationQueueService, + externalNotificationQueueService: externalNotificationQueueService, + activityQueueService: activityQueueService, + eventQueueService: eventQueueService, } } @@ -80,11 +126,16 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment _ = copier.Copy(comment, req) comment.Status = entity.CommentStatusAvailable - // add question id objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return nil, err } + if objInfo.IsDeleted() { + return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) + } + objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID) + objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) + objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) if objInfo.ObjectType == constant.QuestionObjectType || objInfo.ObjectType == constant.AnswerObjectType { comment.QuestionID = objInfo.QuestionID } @@ -109,21 +160,61 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment return nil, err } - if objInfo.ObjectType == constant.QuestionObjectType { - cs.notificationQuestionComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID) - } else if objInfo.ObjectType == constant.AnswerObjectType { - cs.notificationAnswerComment(ctx, objInfo.ObjectCreator, comment.ID, req.UserID) + resp = &schema.GetCommentResp{} + resp.SetFromComment(comment) + resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, + time.Now(), req.CanEdit, req.CanDelete) + + commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo) + if err != nil { + return commentResp, err } - if len(req.MentionUsernameList) > 0 { - cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID) + + // get user info + userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID) + if err != nil { + return nil, err + } + if exist { + resp.Username = userInfo.Username + resp.UserDisplayName = userInfo.DisplayName + resp.UserAvatar = userInfo.Avatar + resp.UserStatus = userInfo.Status } - resp = &schema.GetCommentResp{} - resp.SetFromComment(comment) - resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID) + activityMsg := &schema.ActivityMsg{ + UserID: comment.UserID, + ObjectID: comment.ID, + OriginalObjectID: req.ObjectID, + ActivityTypeKey: constant.ActQuestionCommented, + } + var event *schema.EventMsg + switch objInfo.ObjectType { + case constant.QuestionObjectType: + activityMsg.ActivityTypeKey = constant.ActQuestionCommented + event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). + CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID) + case constant.AnswerObjectType: + activityMsg.ActivityTypeKey = constant.ActAnswerCommented + event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). + CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID) + } + cs.activityQueueService.Send(ctx, activityMsg) + cs.eventQueueService.Send(ctx, event) + return resp, nil +} + +func (cs *CommentService) addCommentNotification( + ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp, + comment *entity.Comment, objInfo *schema.SimpleObjectInfo) (*schema.GetCommentResp, error) { + // The priority of the notification + // 1. reply to user + // 2. comment mention to user + // 3. answer or question was commented + alreadyNotifiedUserID := make(map[string]bool) // get reply user info - if len(resp.ReplyUserID) > 0 { + if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID { replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID) if err != nil { return nil, err @@ -133,40 +224,74 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment resp.ReplyUserDisplayName = replyUser.DisplayName resp.ReplyUserStatus = replyUser.Status } - cs.notificationCommentReply(ctx, replyUser.ID, objInfo.QuestionID, req.UserID) + cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID, + objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) + alreadyNotifiedUserID[replyUser.ID] = true + return nil, nil } - // get user info - userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID) - if err != nil { - return nil, err + if len(req.MentionUsernameList) > 0 { + alreadyNotifiedUserIDs := cs.notificationMention( + ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID) + for _, userID := range alreadyNotifiedUserIDs { + alreadyNotifiedUserID[userID] = true + } + return nil, nil } - if exist { - resp.Username = userInfo.Username - resp.UserDisplayName = userInfo.DisplayName - resp.UserAvatar = userInfo.Avatar - resp.UserStatus = userInfo.Status + + if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { + cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID, + objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) + } else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] { + cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, + objInfo.ObjectCreatorUserID, comment.ID, req.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240)) } - return resp, nil + return nil, nil } // RemoveComment delete comment func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) { - if err := cs.checkCommentWhetherOwner(ctx, req.UserID, req.CommentID); err != nil { + err = cs.commentRepo.RemoveComment(ctx, req.CommentID) + if err != nil { return err } - return cs.commentRepo.RemoveComment(ctx, req.CommentID) + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID). + TID(req.CommentID).CID(req.CommentID, req.UserID)) + return nil } // UpdateComment update comment -func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) { - if err := cs.checkCommentWhetherOwner(ctx, req.UserID, req.CommentID); err != nil { - return err +func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) ( + resp *schema.UpdateCommentResp, err error) { + old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) + if err != nil { + return nil, err } - comment := &entity.Comment{} - _ = copier.Copy(comment, req) - comment.ID = req.CommentID - return cs.commentRepo.UpdateComment(ctx, comment) + if !exist { + return nil, errors.BadRequest(reason.CommentNotFound) + } + // user can't edit the comment that was posted by others except admin + if !req.IsAdmin && req.UserID != old.UserID { + return nil, errors.BadRequest(reason.CommentNotFound) + } + + // user can edit the comment that was posted by himself before deadline. + // admin can edit it at any time + if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) { + return nil, errors.BadRequest(reason.CommentCannotEditAfterDeadline) + } + + if err = cs.commentRepo.UpdateCommentContent(ctx, old.ID, req.OriginalText, req.ParsedText); err != nil { + return nil, err + } + resp = &schema.UpdateCommentResp{ + CommentID: old.ID, + OriginalText: req.OriginalText, + ParsedText: req.ParsedText, + } + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID). + CID(old.ID, old.UserID)) + return resp, nil } // GetComment get comment one @@ -176,7 +301,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment return } if !exist { - return nil, errors.BadRequest(reason.UnknownError) + return nil, errors.BadRequest(reason.CommentNotFound) } resp = &schema.GetCommentResp{ @@ -221,7 +346,8 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment // check if current user vote this comment resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID) - resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID) + resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, + comment.CreatedAt, req.CanEdit, req.CanDelete) return resp, nil } @@ -239,67 +365,86 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge } resp := make([]*schema.GetCommentResp, 0) for _, comment := range commentList { - commentResp := &schema.GetCommentResp{ - CommentID: comment.ID, - CreatedAt: comment.CreatedAt.Unix(), - UserID: comment.UserID, - ReplyUserID: comment.GetReplyUserID(), - ReplyCommentID: comment.GetReplyCommentID(), - ObjectID: comment.ObjectID, - VoteCount: comment.VoteCount, - OriginalText: comment.OriginalText, - ParsedText: comment.ParsedText, + commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment) + if err != nil { + return nil, err } + resp = append(resp, commentResp) + } - // get comment user info - if len(commentResp.UserID) > 0 { - commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID) - if err != nil { - return nil, err - } - if exist { - commentResp.Username = commentUser.Username - commentResp.UserDisplayName = commentUser.DisplayName - commentResp.UserAvatar = commentUser.Avatar - commentResp.UserStatus = commentUser.Status + // if user request the specific comment, add it if not exist. + if len(req.CommentID) > 0 { + commentExist := false + for _, t := range resp { + if t.CommentID == req.CommentID { + commentExist = true + break } } - - // get reply user info - if len(commentResp.ReplyUserID) > 0 { - replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID) + if !commentExist { + comment, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID) if err != nil { return nil, err } - if exist { - commentResp.ReplyUsername = replyUser.Username - commentResp.ReplyUserDisplayName = replyUser.DisplayName - commentResp.ReplyUserStatus = replyUser.Status + if exist && comment.ObjectID == req.ObjectID { + commentResp, err := cs.convertCommentEntity2Resp(ctx, req, comment) + if err != nil { + return nil, err + } + resp = append(resp, commentResp) } } - - // check if current user vote this comment - commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID) - - commentResp.MemberActions = permission.GetCommentPermission(req.UserID, commentResp.UserID) - resp = append(resp, commentResp) } return pager.NewPageModel(total, resp), nil } -func (cs *CommentService) checkCommentWhetherOwner(ctx context.Context, userID, commentID string) error { - // check comment if user self - comment, exist, err := cs.commentCommonRepo.GetComment(ctx, commentID) - if err != nil { - return err +func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *schema.GetCommentWithPageReq, + comment *entity.Comment) (commentResp *schema.GetCommentResp, err error) { + commentResp = &schema.GetCommentResp{ + CommentID: comment.ID, + CreatedAt: comment.CreatedAt.Unix(), + UserID: comment.UserID, + ReplyUserID: comment.GetReplyUserID(), + ReplyCommentID: comment.GetReplyCommentID(), + ObjectID: comment.ObjectID, + VoteCount: comment.VoteCount, + OriginalText: comment.OriginalText, + ParsedText: comment.ParsedText, } - if !exist { - return errors.BadRequest(reason.CommentNotFound) + + // get comment user info + if len(commentResp.UserID) > 0 { + commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID) + if err != nil { + return nil, err + } + if exist { + commentResp.Username = commentUser.Username + commentResp.UserDisplayName = commentUser.DisplayName + commentResp.UserAvatar = commentUser.Avatar + commentResp.UserStatus = commentUser.Status + } } - if comment.UserID != userID { - return errors.BadRequest(reason.CommentEditWithoutPermission) + + // get reply user info + if len(commentResp.ReplyUserID) > 0 { + replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID) + if err != nil { + return nil, err + } + if exist { + commentResp.ReplyUsername = replyUser.Username + commentResp.ReplyUserDisplayName = replyUser.DisplayName + commentResp.ReplyUserStatus = replyUser.Status + } } - return nil + + // check if current user vote this comment + commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID) + + commentResp.MemberActions = permission.GetCommentPermission(ctx, + req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete) + return commentResp, nil } func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID string) (isVote bool) { @@ -348,8 +493,12 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s } else { commentResp.ObjectType = objInfo.ObjectType commentResp.Title = objInfo.Title + commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title) commentResp.QuestionID = objInfo.QuestionID commentResp.AnswerID = objInfo.AnswerID + if objInfo.QuestionStatus == entity.QuestionStatusDeleted { + commentResp.Title = "Deleted question" + } } } resp = append(resp, commentResp) @@ -357,7 +506,12 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s return pager.NewPageModel(total, resp), nil } -func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, commentID, commentUserID string) { +func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, + questionID, questionTitle, commentID, commentUserID, commentSummary string) { + if questionUserID == commentUserID { + return + } + // send internal notification msg := &schema.NotificationMsg{ ReceiverUserID: questionUserID, TriggerUserID: commentUserID, @@ -365,11 +519,47 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType - msg.NotificationAction = constant.CommentQuestion - notice_queue.AddNotification(msg) + msg.NotificationAction = constant.NotificationCommentQuestion + cs.notificationQueueService.Send(ctx, msg) + + // send external notification + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewCommentTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + CommentID: commentID, + CommentSummary: commentSummary, + UnsubscribeCode: token.GenerateToken(), + } + commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) + if commentUser != nil { + rawData.CommentUserDisplayName = commentUser.DisplayName + } + externalNotificationMsg.NewCommentTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } -func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerUserID, commentID, commentUserID string) { +func (cs *CommentService) notificationAnswerComment(ctx context.Context, + questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) { + if answerUserID == commentUserID { + return + } + + // Send internal notification. msg := &schema.NotificationMsg{ ReceiverUserID: answerUserID, TriggerUserID: commentUserID, @@ -377,11 +567,42 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context, answerU ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType - msg.NotificationAction = constant.CommentAnswer - notice_queue.AddNotification(msg) + msg.NotificationAction = constant.NotificationCommentAnswer + cs.notificationQueueService.Send(ctx, msg) + + // Send external notification. + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", answerUserID) + return + } + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewCommentTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + AnswerID: answerID, + CommentID: commentID, + CommentSummary: commentSummary, + UnsubscribeCode: token.GenerateToken(), + } + commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) + if commentUser != nil { + rawData.CommentUserDisplayName = commentUser.DisplayName + } + externalNotificationMsg.NewCommentTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } -func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID string) { +func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID, + questionID, questionTitle, commentSummary string) { msg := &schema.NotificationMsg{ ReceiverUserID: replyUserID, TriggerUserID: commentUserID, @@ -389,18 +610,49 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType - msg.NotificationAction = constant.ReplyToYou - notice_queue.AddNotification(msg) + msg.NotificationAction = constant.NotificationReplyToYou + cs.notificationQueueService.Send(ctx, msg) + + // Send external notification. + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", replyUserID) + return + } + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewCommentTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + CommentID: commentID, + CommentSummary: commentSummary, + UnsubscribeCode: token.GenerateToken(), + } + commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID) + if commentUser != nil { + rawData.CommentUserDisplayName = commentUser.DisplayName + } + externalNotificationMsg.NewCommentTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) } -func (cs *CommentService) notificationMention(ctx context.Context, mentionUsernameList []string, commentID, commentUserID string) { +func (cs *CommentService) notificationMention( + ctx context.Context, mentionUsernameList []string, commentID, commentUserID string, + alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) { for _, username := range mentionUsernameList { userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username) if err != nil { log.Error(err) continue } - if exist { + if exist && !alreadyNotifiedUserID[userInfo.ID] { msg := &schema.NotificationMsg{ ReceiverUserID: userInfo.ID, TriggerUserID: commentUserID, @@ -408,8 +660,10 @@ func (cs *CommentService) notificationMention(ctx context.Context, mentionUserna ObjectID: commentID, } msg.ObjectType = constant.CommentObjectType - msg.NotificationAction = constant.MentionYou - notice_queue.AddNotification(msg) + msg.NotificationAction = constant.NotificationMentionYou + cs.notificationQueueService.Send(ctx, msg) + alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID) } } + return alreadyNotifiedUserIDs } diff --git a/internal/service/comment_common/comment_service.go b/internal/service/comment_common/comment_service.go index dc1f9fc9e..0b6423a42 100644 --- a/internal/service/comment_common/comment_service.go +++ b/internal/service/comment_common/comment_service.go @@ -1,17 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package comment_common import ( "context" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" "github.com/segmentfault/pacman/errors" ) // CommentCommonRepo comment repository type CommentCommonRepo interface { GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) + GetCommentWithoutStatus(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) + GetCommentCount(ctx context.Context) (count int64, err error) + RemoveAllUserComment(ctx context.Context, userID string) (err error) } // CommentCommonService user service @@ -34,7 +56,7 @@ func (cs *CommentCommonService) GetComment(ctx context.Context, commentID string return } if !exist { - return nil, errors.BadRequest(reason.UnknownError) + return nil, errors.BadRequest(reason.CommentNotFound) } resp = &schema.GetCommentResp{} diff --git a/internal/service/config/config_service.go b/internal/service/config/config_service.go index 818b7f28b..63aea221f 100644 --- a/internal/service/config/config_service.go +++ b/internal/service/config/config_service.go @@ -1,14 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package config +import ( + "context" + "encoding/json" + "fmt" + + "github.com/apache/answer/internal/entity" +) + // ConfigRepo config repository type ConfigRepo interface { - Get(key string) (interface{}, error) - GetString(key string) (string, error) - GetInt(key string) (int, error) - GetArrayString(key string) ([]string, error) - GetConfigType(key string) (int, error) - GetConfigById(id int, value any) (err error) - SetConfig(key, value string) (err error) + GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) + GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) + UpdateConfig(ctx context.Context, key, value string) (err error) } // ConfigService user service @@ -16,8 +39,70 @@ type ConfigService struct { configRepo ConfigRepo } +// NewConfigService new config service func NewConfigService(configRepo ConfigRepo) *ConfigService { return &ConfigService{ configRepo: configRepo, } } + +// GetIntValue get config int value +func (cs *ConfigService) GetIntValue(ctx context.Context, key string) (val int, err error) { + cf, err := cs.configRepo.GetConfigByKey(ctx, key) + if err != nil { + return 0, err + } + return cf.GetIntValue(), nil +} + +// GetStringValue get config string value +func (cs *ConfigService) GetStringValue(ctx context.Context, key string) (val string, err error) { + cf, err := cs.configRepo.GetConfigByKey(ctx, key) + if err != nil { + return "", err + } + return cf.Value, nil +} + +// GetArrayStringValue get config array string value +func (cs *ConfigService) GetArrayStringValue(ctx context.Context, key string) (val []string, err error) { + cf, err := cs.configRepo.GetConfigByKey(ctx, key) + if err != nil { + return nil, err + } + return cf.GetArrayStringValue(), nil +} + +func (cs *ConfigService) GetJsonConfigByIDAndSetToObject(ctx context.Context, id int, obj any) (err error) { + cf, err := cs.configRepo.GetConfigByID(ctx, id) + if err != nil { + return err + } + err = json.Unmarshal([]byte(cf.Value), obj) + if err != nil { + return fmt.Errorf("[%s] config value is not json format", cf.Key) + } + return nil +} + +// GetConfigByID get config by id +func (cs *ConfigService) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) { + return cs.configRepo.GetConfigByID(ctx, id) +} + +func (cs *ConfigService) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) { + return cs.configRepo.GetConfigByKey(ctx, key) +} + +// GetIDByKey get config id by key +func (cs *ConfigService) GetIDByKey(ctx context.Context, key string) (id int, err error) { + cf, err := cs.configRepo.GetConfigByKey(ctx, key) + if err != nil { + return 0, err + } + return cf.ID, nil +} + +func (cs *ConfigService) UpdateConfig(ctx context.Context, key, value string) (err error) { + return cs.configRepo.UpdateConfig(ctx, key, value) +} diff --git a/internal/service/content/answer_service.go b/internal/service/content/answer_service.go new file mode 100644 index 000000000..b9d45b522 --- /dev/null +++ b/internal/service/content/answer_service.go @@ -0,0 +1,746 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + "encoding/json" + "time" + + "github.com/apache/answer/internal/service/event_queue" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + answercommon "github.com/apache/answer/internal/service/answer_common" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/permission" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/role" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// AnswerService user service +type AnswerService struct { + answerRepo answercommon.AnswerRepo + questionRepo questioncommon.QuestionRepo + questionCommon *questioncommon.QuestionCommon + answerActivityService *activity.AnswerActivityService + userCommon *usercommon.UserCommon + collectionCommon *collectioncommon.CollectionCommon + userRepo usercommon.UserRepo + revisionService *revision_common.RevisionService + AnswerCommon *answercommon.AnswerCommon + voteRepo activity_common.VoteRepo + emailService *export.EmailService + roleService *role.UserRoleRelService + notificationQueueService notice_queue.NotificationQueueService + externalNotificationQueueService notice_queue.ExternalNotificationQueueService + activityQueueService activity_queue.ActivityQueueService + reviewService *review.ReviewService + eventQueueService event_queue.EventQueueService +} + +func NewAnswerService( + answerRepo answercommon.AnswerRepo, + questionRepo questioncommon.QuestionRepo, + questionCommon *questioncommon.QuestionCommon, + userCommon *usercommon.UserCommon, + collectionCommon *collectioncommon.CollectionCommon, + userRepo usercommon.UserRepo, + revisionService *revision_common.RevisionService, + answerAcceptActivityRepo *activity.AnswerActivityService, + answerCommon *answercommon.AnswerCommon, + voteRepo activity_common.VoteRepo, + emailService *export.EmailService, + roleService *role.UserRoleRelService, + notificationQueueService notice_queue.NotificationQueueService, + externalNotificationQueueService notice_queue.ExternalNotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, + reviewService *review.ReviewService, + eventQueueService event_queue.EventQueueService, +) *AnswerService { + return &AnswerService{ + answerRepo: answerRepo, + questionRepo: questionRepo, + userCommon: userCommon, + collectionCommon: collectionCommon, + questionCommon: questionCommon, + userRepo: userRepo, + revisionService: revisionService, + answerActivityService: answerAcceptActivityRepo, + AnswerCommon: answerCommon, + voteRepo: voteRepo, + emailService: emailService, + roleService: roleService, + notificationQueueService: notificationQueueService, + externalNotificationQueueService: externalNotificationQueueService, + activityQueueService: activityQueueService, + reviewService: reviewService, + eventQueueService: eventQueueService, + } +} + +// RemoveAnswer delete answer +func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAnswerReq) (err error) { + answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID) + if err != nil { + return err + } + if !exist { + return nil + } + // if the status is deleted, return directly + if answerInfo.Status == entity.AnswerStatusDeleted { + return nil + } + roleID, err := as.roleService.GetUserRole(ctx, req.UserID) + if err != nil { + return err + } + if roleID != role.RoleAdminID && roleID != role.RoleModeratorID { + if answerInfo.UserID != req.UserID { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if answerInfo.VoteCount > 0 { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if answerInfo.Accepted == schema.AnswerAcceptedEnable { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + _, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + if !exist { + return errors.BadRequest(reason.AnswerCannotDeleted) + } + + } + + err = as.answerRepo.RemoveAnswer(ctx, req.ID) + if err != nil { + return err + } + + // user add question count + err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID) + if err != nil { + log.Error("IncreaseAnswerCount error", err.Error()) + } + userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) + if err != nil { + log.Error("GetCountByUserID error", err.Error()) + } + err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) + if err != nil { + log.Error("user IncreaseAnswerCount error", err.Error()) + } + err = as.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: answerInfo.QuestionID, + FromAnswerID: answerInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: answerInfo.QuestionID, + ToAnswerID: answerInfo.ID, + }) + if err != nil { + log.Error("RemoveQuestionLink error", err.Error()) + } + + // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, + // facing the problem of recovery. + //err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount) + //if err != nil { + // log.Errorf("delete answer activity change failed: %s", err.Error()) + //} + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: answerInfo.ID, + OriginalObjectID: answerInfo.ID, + ActivityTypeKey: constant.ActAnswerDeleted, + }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID). + AID(answerInfo.ID, answerInfo.UserID)) + return +} + +// RecoverAnswer recover deleted answer +func (as *AnswerService) RecoverAnswer(ctx context.Context, req *schema.RecoverAnswerReq) (err error) { + answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.AnswerNotFound) + } + if answerInfo.Status != entity.AnswerStatusDeleted { + return nil + } + if err = as.answerRepo.RecoverAnswer(ctx, req.AnswerID); err != nil { + return err + } + if err = as.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: answerInfo.QuestionID, + FromAnswerID: answerInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: answerInfo.QuestionID, + ToAnswerID: answerInfo.ID, + }); err != nil { + return err + } + + if err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil { + log.Errorf("update answer count failed: %s", err.Error()) + } + userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) + if err != nil { + log.Errorf("get user answer count failed: %s", err.Error()) + } else { + err = as.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) + if err != nil { + log.Errorf("update user answer count failed: %s", err.Error()) + } + } + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: answerInfo.ID, + OriginalObjectID: answerInfo.ID, + ActivityTypeKey: constant.ActAnswerUndeleted, + }) + return nil +} + +func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) { + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return "", err + } + if !exist { + return "", errors.BadRequest(reason.QuestionNotFound) + } + if questionInfo.Status == entity.QuestionStatusClosed || questionInfo.Status == entity.QuestionStatusDeleted { + err = errors.BadRequest(reason.AnswerCannotAddByClosedQuestion) + return "", err + } + insertData := &entity.Answer{} + insertData.UserID = req.UserID + insertData.OriginalText = req.Content + insertData.ParsedText = req.HTML + insertData.Accepted = schema.AnswerAcceptedFailed + insertData.QuestionID = req.QuestionID + insertData.RevisionID = "0" + insertData.LastEditUserID = "0" + insertData.Status = entity.AnswerStatusPending + //insertData.UpdatedAt = now + if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil { + return "", err + } + insertData.Status = as.reviewService.AddAnswerReview(ctx, insertData, req.IP, req.UserAgent) + if err := as.answerRepo.UpdateAnswerStatus(ctx, insertData.ID, insertData.Status); err != nil { + return "", err + } + if insertData.Status == entity.AnswerStatusAvailable { + insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText) + if err != nil { + return "", err + } + if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"parsed_text"}); err != nil { + return "", err + } + } + err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID) + if err != nil { + log.Error("IncreaseAnswerCount error", err.Error()) + } + err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, uid.DeShortID(insertData.ID)) + if err != nil { + log.Error("UpdateLastAnswer error", err.Error()) + } + err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID) + if err != nil { + return insertData.ID, err + } + userAnswerCount, err := as.answerRepo.GetCountByUserID(ctx, req.UserID) + if err != nil { + log.Error("GetCountByUserID error", err.Error()) + } + err = as.userCommon.UpdateAnswerCount(ctx, req.UserID, int(userAnswerCount)) + if err != nil { + log.Error("user IncreaseAnswerCount error", err.Error()) + } + + revisionDTO := &schema.AddRevisionDTO{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + Title: "", + } + infoJSON, _ := json.Marshal(insertData) + revisionDTO.Content = string(infoJSON) + revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return insertData.ID, err + } + if insertData.Status == entity.AnswerStatusAvailable { + as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title, + htmltext.FetchExcerpt(insertData.ParsedText, "...", 240)) + } + + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerAnswered, + RevisionID: revisionID, + }) + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: insertData.UserID, + ObjectID: insertData.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionAnswered, + }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID). + AID(insertData.ID, insertData.UserID)) + return insertData.ID, nil +} + +func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) { + var canUpdate bool + _, existUnreviewed, err := as.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return "", err + } + if existUnreviewed { + return "", errors.BadRequest(reason.AnswerCannotUpdate) + } + + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return "", err + } + if !exist { + return "", errors.BadRequest(reason.QuestionNotFound) + } + + answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID) + if err != nil { + return "", err + } + if !exist { + return "", errors.BadRequest(reason.AnswerNotFound) + } + + if answerInfo.Status == entity.AnswerStatusDeleted { + return "", errors.BadRequest(reason.AnswerCannotUpdate) + } + + //If the content is the same, ignore it + if answerInfo.OriginalText == req.Content { + return "", nil + } + + insertData := &entity.Answer{} + insertData.ID = req.ID + insertData.UserID = answerInfo.UserID + insertData.QuestionID = req.QuestionID + insertData.OriginalText = req.Content + insertData.ParsedText = req.HTML + insertData.UpdatedAt = time.Now() + insertData.LastEditUserID = "0" + if answerInfo.UserID != req.UserID { + insertData.LastEditUserID = req.UserID + } + + revisionDTO := &schema.AddRevisionDTO{ + UserID: req.UserID, + ObjectID: req.ID, + Log: req.EditSummary, + } + + if req.NoNeedReview || answerInfo.UserID == req.UserID { + canUpdate = true + } + + if !canUpdate { + revisionDTO.Status = entity.RevisionUnreviewedStatus + } else { + insertData.ParsedText, err = as.questionCommon.UpdateQuestionLink(ctx, insertData.QuestionID, insertData.ID, insertData.ParsedText, insertData.OriginalText) + if err != nil { + return "", err + } + if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}); err != nil { + return "", err + } + err = as.questionCommon.UpdatePostTime(ctx, req.QuestionID) + if err != nil { + return insertData.ID, err + } + as.notificationUpdateAnswer(ctx, questionInfo.UserID, insertData.ID, req.UserID) + revisionDTO.Status = entity.RevisionReviewPassStatus + } + + infoJSON, _ := json.Marshal(insertData) + revisionDTO.Content = string(infoJSON) + revisionID, err := as.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return insertData.ID, err + } + if canUpdate { + as.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerEdited, + RevisionID: revisionID, + }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID). + AID(insertData.ID, insertData.UserID)) + } + + return insertData.ID, nil +} + +// AcceptAnswer accept answer +func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAnswerReq) (err error) { + // find question + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + questionInfo.ID = uid.DeShortID(questionInfo.ID) + if questionInfo.AcceptedAnswerID == req.AnswerID { + return nil + } + + // find answer + var acceptedAnswerInfo *entity.Answer + if len(req.AnswerID) > 1 { + acceptedAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, req.AnswerID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.AnswerNotFound) + } + acceptedAnswerInfo.ID = uid.DeShortID(acceptedAnswerInfo.ID) + } + + // update answers status + if err = as.answerRepo.UpdateAcceptedStatus(ctx, req.AnswerID, req.QuestionID); err != nil { + return err + } + + // update question status + err = as.questionCommon.UpdateAccepted(ctx, req.QuestionID, req.AnswerID) + if err != nil { + log.Error("UpdateLastAnswer error", err.Error()) + } + + var oldAnswerInfo *entity.Answer + if len(questionInfo.AcceptedAnswerID) > 1 { + oldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID) + if err != nil { + return err + } + oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID) + } + + if acceptedAnswerInfo != nil { + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID). + QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID)) + } + + as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo) + return nil +} + +func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, + questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer, +) { + // if this question is already been answered, should cancel old answer rank + if oldAnswerInfo != nil { + err := as.answerActivityService.CancelAcceptAnswer(ctx, userID, + questionInfo.AcceptedAnswerID, questionInfo.ID, questionInfo.UserID, oldAnswerInfo.UserID) + if err != nil { + log.Error(err) + } + } + if newAnswerInfo != nil { + err := as.answerActivityService.AcceptAnswer(ctx, userID, newAnswerInfo.ID, + questionInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == questionInfo.UserID) + if err != nil { + log.Error(err) + } + } +} + +func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) { + answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) + if err != nil { + return nil, nil, has, err + } + info := as.ShowFormat(ctx, answerInfo) + // todo questionFunc + questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID) + if err != nil { + return nil, nil, has, err + } + // todo UserFunc + + userIds := make([]string, 0) + userIds = append(userIds, answerInfo.UserID) + userIds = append(userIds, answerInfo.LastEditUserID) + userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds) + if err != nil { + return nil, nil, has, err + } + + _, ok := userInfoMap[answerInfo.UserID] + if ok { + info.UserInfo = userInfoMap[answerInfo.UserID] + } + _, ok = userInfoMap[answerInfo.LastEditUserID] + if ok { + info.UpdateUserInfo = userInfoMap[answerInfo.LastEditUserID] + } + + if loginUserID == "" { + return info, questionInfo, has, nil + } + + info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID) + + collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID}) + if err != nil { + return nil, nil, has, err + } + if len(collectedMap) > 0 { + info.Collected = true + } + + return info, questionInfo, has, nil +} + +func (as *AnswerService) GetDetail(ctx context.Context, answerID string) (*schema.AnswerInfo, error) { + answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) + if err != nil { + return nil, err + } + if !has { + return nil, errors.BadRequest(reason.AnswerNotFound) + } + info := as.ShowFormat(ctx, answerInfo) + return info, nil +} + +func (as *AnswerService) GetCountByUserIDQuestionID(ctx context.Context, userId string, questionId string) (ids []string, err error) { + return as.answerRepo.GetIDsByUserIDAndQuestionID(ctx, userId, questionId) +} + +func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminUpdateAnswerStatusReq) error { + setStatus, ok := entity.AdminAnswerSearchStatus[req.Status] + if !ok { + return errors.BadRequest(reason.RequestFormatError) + } + answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.AnswerNotFound) + } + + if setStatus == entity.AnswerStatusDeleted { + if err := as.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ + ID: req.AnswerID, + UserID: req.UserID, + CanDelete: true, + }); err != nil { + return err + } + + msg := &schema.NotificationMsg{} + msg.ObjectID = answerInfo.ID + msg.Type = schema.NotificationTypeInbox + msg.ReceiverUserID = answerInfo.UserID + msg.TriggerUserID = answerInfo.UserID + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationYourAnswerWasDeleted + as.notificationQueueService.Send(ctx, msg) + } + + // recover + if setStatus == entity.QuestionStatusAvailable && answerInfo.Status == entity.QuestionStatusDeleted { + if err := as.RecoverAnswer(ctx, &schema.RecoverAnswerReq{ + AnswerID: req.AnswerID, + UserID: req.UserID, + }); err != nil { + return err + } + } + return nil +} + +func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) { + list := make([]*schema.AnswerInfo, 0) + dbSearch := entity.AnswerSearch{} + dbSearch.QuestionID = req.QuestionID + dbSearch.Page = req.Page + dbSearch.PageSize = req.PageSize + dbSearch.Order = req.Order + dbSearch.IncludeDeleted = req.CanDelete + dbSearch.LoginUserID = req.UserID + answerOriginalList, count, err := as.answerRepo.SearchList(ctx, &dbSearch) + if err != nil { + return list, count, err + } + answerList, err := as.SearchFormatInfo(ctx, answerOriginalList, req) + if err != nil { + return answerList, count, err + } + return answerList, count, nil +} + +func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity.Answer, req *schema.AnswerListReq) ( + []*schema.AnswerInfo, error) { + list := make([]*schema.AnswerInfo, 0) + objectIDs := make([]string, 0) + userIDs := make([]string, 0) + for _, info := range answers { + item := as.ShowFormat(ctx, info) + list = append(list, item) + objectIDs = append(objectIDs, info.ID) + userIDs = append(userIDs, info.UserID, info.LastEditUserID) + } + + userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs) + if err != nil { + return list, err + } + for _, item := range list { + item.UserInfo = userInfoMap[item.UserID] + item.UpdateUserInfo = userInfoMap[item.UpdateUserID] + } + if len(req.UserID) == 0 { + return list, nil + } + + collectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, req.UserID, objectIDs) + if err != nil { + return nil, err + } + for _, item := range list { + item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, req.UserID) + item.Collected = collectedMap[item.ID] + item.MemberActions = permission.GetAnswerPermission(ctx, + req.UserID, + item.UserID, + item.Status, + req.CanEdit, + req.CanDelete, + req.CanRecover) + } + return list, nil +} + +func (as *AnswerService) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { + return as.AnswerCommon.ShowFormat(ctx, data) +} + +func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionUserID, answerID, answerUserID string) { + // If the answer is updated by me, there is no notification for myself. + // equivalent behaviour as AnswerService.notificationAnswerTheQuestion + if questionUserID == answerUserID { + return + } + msg := &schema.NotificationMsg{ + TriggerUserID: answerUserID, + ReceiverUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationUpdateAnswer + as.notificationQueueService.Send(ctx, msg) +} + +func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, + questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) { + // If the question is answered by me, there is no notification for myself. + if questionUserID == answerUserID { + return + } + msg := &schema.NotificationMsg{ + TriggerUserID: answerUserID, + ReceiverUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationAnswerTheQuestion + as.notificationQueueService.Send(ctx, msg) + + receiverUserInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewAnswerTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + AnswerID: answerID, + AnswerSummary: answerSummary, + UnsubscribeCode: token.GenerateToken(), + } + answerUser, _, _ := as.userCommon.GetUserBasicInfoByID(ctx, answerUserID) + if answerUser != nil { + rawData.AnswerUserDisplayName = answerUser.DisplayName + } + externalNotificationMsg.NewAnswerTemplateRawData = rawData + as.externalNotificationQueueService.Send(ctx, externalNotificationMsg) +} diff --git a/internal/service/content/question_hottest_service.go b/internal/service/content/question_hottest_service.go new file mode 100644 index 000000000..a33b155fb --- /dev/null +++ b/internal/service/content/question_hottest_service.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" + "math" + "time" +) + +func (q *QuestionService) RefreshHottestCron(ctx context.Context) { + + var ( + page = 1 + pageSize = 100 + ) + + for { + questionList, _, err := q.questionRepo.GetQuestionPage( + ctx, + page, pageSize, + []string{}, + "", "newest", + schema.HotInDays, + false, false) + if err != nil { + return + } + + for _, question := range questionList { + updatedAt := question.UpdatedAt.Unix() + if updatedAt < 0 { + updatedAt = question.CreatedAt.Unix() + } + + qAgeInHours := (time.Now().Unix() - question.CreatedAt.Unix()) / 3600 + qUpdated := (time.Now().Unix() - updatedAt) / 3600 + + aScores, err := q.answerRepo.SumVotesByQuestionID(ctx, question.ID) + if err != nil { + aScores = 0 + } + + score := q.getScore(float64(question.ViewCount), float64(question.AnswerCount), float64(question.VoteCount), aScores, float64(qAgeInHours), float64(qUpdated)) + if score < 0 { + score = 0 + } + + questioninfo := &entity.Question{} + questioninfo.ID = question.ID + questioninfo.HotScore = int(math.Ceil(score * 10000)) + err = q.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"hot_score"}) + if err != nil { + log.Error("update question hot score error,question ID:", question.ID, " error: ", err) + } + } + + if len(questionList) < pageSize { + break + } + page++ + } +} + +func (q *QuestionService) getScore(qViews, qAnswers, qScore, aScores, qAgeInHours, qUpdated float64) (score float64) { + score = ((math.Log(qViews) * 4) + ((qAnswers * qScore) / 5) + aScores) / + math.Pow(((qAgeInHours+1)-((qAgeInHours-qUpdated)/2)), 1.5) + return score +} diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go new file mode 100644 index 000000000..ec9ce48e9 --- /dev/null +++ b/internal/service/content/question_service.go @@ -0,0 +1,1687 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/apache/answer/internal/service/event_queue" + "github.com/apache/answer/plugin" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + answercommon "github.com/apache/answer/internal/service/answer_common" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/export" + metacommon "github.com/apache/answer/internal/service/meta_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/notification" + "github.com/apache/answer/internal/service/permission" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/tag" + tagcommon "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/pkg/uid" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "golang.org/x/net/context" +) + +// QuestionRepo question repository + +// QuestionService user service +type QuestionService struct { + activityRepo activity_common.ActivityRepo + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + tagCommon *tagcommon.TagCommonService + tagService *tag.TagService + questioncommon *questioncommon.QuestionCommon + userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo + userRoleRelService *role.UserRoleRelService + revisionService *revision_common.RevisionService + metaService *metacommon.MetaCommonService + collectionCommon *collectioncommon.CollectionCommon + answerActivityService *activity.AnswerActivityService + emailService *export.EmailService + notificationQueueService notice_queue.NotificationQueueService + externalNotificationQueueService notice_queue.ExternalNotificationQueueService + activityQueueService activity_queue.ActivityQueueService + siteInfoService siteinfo_common.SiteInfoCommonService + newQuestionNotificationService *notification.ExternalNotificationService + reviewService *review.ReviewService + configService *config.ConfigService + eventQueueService event_queue.EventQueueService + reviewRepo review.ReviewRepo +} + +func NewQuestionService( + activityRepo activity_common.ActivityRepo, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + tagCommon *tagcommon.TagCommonService, + tagService *tag.TagService, + questioncommon *questioncommon.QuestionCommon, + userCommon *usercommon.UserCommon, + userRepo usercommon.UserRepo, + userRoleRelService *role.UserRoleRelService, + revisionService *revision_common.RevisionService, + metaService *metacommon.MetaCommonService, + collectionCommon *collectioncommon.CollectionCommon, + answerActivityService *activity.AnswerActivityService, + emailService *export.EmailService, + notificationQueueService notice_queue.NotificationQueueService, + externalNotificationQueueService notice_queue.ExternalNotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, + siteInfoService siteinfo_common.SiteInfoCommonService, + newQuestionNotificationService *notification.ExternalNotificationService, + reviewService *review.ReviewService, + configService *config.ConfigService, + eventQueueService event_queue.EventQueueService, + reviewRepo review.ReviewRepo, +) *QuestionService { + return &QuestionService{ + activityRepo: activityRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + tagCommon: tagCommon, + tagService: tagService, + questioncommon: questioncommon, + userCommon: userCommon, + userRepo: userRepo, + userRoleRelService: userRoleRelService, + revisionService: revisionService, + metaService: metaService, + collectionCommon: collectionCommon, + answerActivityService: answerActivityService, + emailService: emailService, + notificationQueueService: notificationQueueService, + externalNotificationQueueService: externalNotificationQueueService, + activityQueueService: activityQueueService, + siteInfoService: siteInfoService, + newQuestionNotificationService: newQuestionNotificationService, + reviewService: reviewService, + configService: configService, + eventQueueService: eventQueueService, + reviewRepo: reviewRepo, + } +} + +func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + if !has { + return nil + } + + cf, err := qs.configService.GetConfigByID(ctx, req.CloseType) + if err != nil || cf == nil { + return errors.BadRequest(reason.ReportNotFound) + } + if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.CloseMsg) { + return errors.BadRequest(reason.InvalidURLError) + } + + questionInfo.Status = entity.QuestionStatusClosed + err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) + if err != nil { + return err + } + + closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{ + CloseType: req.CloseType, + CloseMsg: req.CloseMsg, + }) + err = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) + if err != nil { + return err + } + if cf.Key == constant.ReasonADuplicate { + qs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg) + } + + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) + return nil +} + +// ReopenQuestion reopen question +func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.ReopenQuestionReq) error { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return err + } + if !has { + return nil + } + + questionInfo.Status = entity.QuestionStatusAvailable + err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) + if err != nil { + return err + } + qs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo) + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionReopened, + }) + return nil +} + +func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*entity.Tag) ([]string, error) { + list := make([]string, 0) + for _, tag := range Tags { + if tag.Reserved { + list = append(list, tag.DisplayName) + } + } + if len(list) > 0 { + return list, errors.BadRequest(reason.RequestFormatError) + } + return []string{}, nil +} +func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) { + if len(req.Tags) == 0 { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) + if err != nil { + return + } + if !recommendExist { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + + tagNameList := make([]string, 0) + for _, tag := range req.Tags { + tagNameList = append(tagNameList, tag.SlugName) + } + Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + return errorlist, tagerr + } + if !req.QuestionPermission.CanUseReservedTag { + taglist, err := qs.AddQuestionCheckTags(ctx, Tags) + errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, + strings.Join(taglist, ",")) + if err != nil { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + } + return nil, nil +} + +// HasNewTag +func (qs *QuestionService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) { + return qs.tagCommon.HasNewTag(ctx, tags) +} + +// AddQuestion add question +func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) { + if len(req.Tags) == 0 { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) + if err != nil { + return + } + if !recommendExist { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + + tagNameList := make([]string, 0) + for _, tag := range req.Tags { + tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") + tagNameList = append(tagNameList, tag.SlugName) + } + tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + return questionInfo, tagerr + } + if !req.QuestionPermission.CanUseReservedTag { + taglist, err := qs.AddQuestionCheckTags(ctx, tags) + errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, + strings.Join(taglist, ",")) + if err != nil { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + } + + question := &entity.Question{} + now := time.Now() + question.UserID = req.UserID + question.Title = req.Title + question.OriginalText = req.Content + question.ParsedText = req.HTML + question.AcceptedAnswerID = "0" + question.LastAnswerID = "0" + question.LastEditUserID = "0" + //question.PostUpdateTime = nil + question.Status = entity.QuestionStatusPending + question.RevisionID = "0" + question.CreatedAt = now + question.PostUpdateTime = now + question.Pin = entity.QuestionUnPin + question.Show = entity.QuestionShow + //question.UpdatedAt = nil + err = qs.questionRepo.AddQuestion(ctx, question) + if err != nil { + return + } + question.Status = qs.reviewService.AddQuestionReview(ctx, question, req.Tags, req.IP, req.UserAgent) + if err := qs.questionRepo.UpdateQuestionStatus(ctx, question.ID, question.Status); err != nil { + return nil, err + } + if question.Status == entity.QuestionStatusAvailable { + question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText) + if err != nil { + return nil, err + } + err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"parsed_text"}) + if err != nil { + return nil, err + } + } + objectTagData := schema.TagChange{} + objectTagData.ObjectID = question.ID + objectTagData.Tags = req.Tags + objectTagData.UserID = req.UserID + err = qs.ChangeTag(ctx, &objectTagData) + if err != nil { + return + } + _ = qs.questionRepo.UpdateSearch(ctx, question.ID) + + revisionDTO := &schema.AddRevisionDTO{ + UserID: question.UserID, + ObjectID: question.ID, + Title: question.Title, + } + + questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, tags) + if err != nil { + return nil, err + } + infoJSON, _ := json.Marshal(questionWithTagsRevision) + revisionDTO.Content = string(infoJSON) + revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return + } + + // user add question count + userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID) + if err != nil { + log.Errorf("get user question count error %v", err) + } else { + err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount) + if err != nil { + log.Errorf("update user question count error %v", err) + } + } + + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: question.UserID, + ObjectID: question.ID, + OriginalObjectID: question.ID, + ActivityTypeKey: constant.ActQuestionAsked, + RevisionID: revisionID, + }) + + if question.Status == entity.QuestionStatusAvailable { + qs.externalNotificationQueueService.Send(ctx, + schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags)) + } + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID). + QID(question.ID, question.UserID)) + + questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) + return +} + +// OperationQuestion +func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + if !has { + return nil + } + // Hidden question cannot be placed at the top + if questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin { + return nil + } + // Question cannot be hidden when they are at the top + if questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide { + return nil + } + + switch req.Operation { + case schema.QuestionOperationHide: + questionInfo.Show = entity.QuestionHide + err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: questionInfo.ID, + }) + if err != nil { + return + } + err = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID) + if err != nil { + return err + } + err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID) + if err != nil { + return err + } + case schema.QuestionOperationShow: + questionInfo.Show = entity.QuestionShow + err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: questionInfo.ID, + }) + if err != nil { + return + } + err = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID) + if err != nil { + return err + } + err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID) + if err != nil { + return err + } + case schema.QuestionOperationPin: + questionInfo.Pin = entity.QuestionPin + case schema.QuestionOperationUnPin: + questionInfo.Pin = entity.QuestionUnPin + } + + err = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo) + if err != nil { + return err + } + + actMap := make(map[string]constant.ActivityTypeKey) + actMap[schema.QuestionOperationPin] = constant.ActQuestionPin + actMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin + actMap[schema.QuestionOperationHide] = constant.ActQuestionHide + actMap[schema.QuestionOperationShow] = constant.ActQuestionShow + _, ok := actMap[req.Operation] + if ok { + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: actMap[req.Operation], + }) + } + + return nil +} + +// RemoveQuestion delete question +func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + //if the status is deleted, return directly + if questionInfo.Status == entity.QuestionStatusDeleted { + return nil + } + if !has { + return nil + } + if !req.IsAdmin { + if questionInfo.UserID != req.UserID { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + + if questionInfo.AcceptedAnswerID != "0" { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + if questionInfo.AnswerCount > 1 { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + + if questionInfo.AnswerCount == 1 { + answersearch := &entity.AnswerSearch{} + answersearch.QuestionID = req.ID + answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) + if err != nil { + return err + } + for _, answer := range answerList { + if answer.VoteCount > 0 { + return errors.BadRequest(reason.QuestionCannotDeleted) + } + } + } + } + + questionInfo.Status = entity.QuestionStatusDeleted + err = qs.questionRepo.UpdateQuestionStatusWithOutUpdateTime(ctx, questionInfo) + if err != nil { + return err + } + + userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID) + if err != nil { + log.Error("user GetUserQuestionCount error", err.Error()) + } else { + err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) + if err != nil { + log.Error("user IncreaseQuestionCount error", err.Error()) + } + } + + // If this question has been reviewed, then delete the review. + reviewInfo, exist, err := qs.reviewRepo.GetReviewByObject(ctx, questionInfo.ID) + if exist && err == nil { + err = qs.reviewRepo.UpdateReviewStatus(ctx, reviewInfo.ID, req.UserID, entity.ReviewStatusRejected) + if err != nil { + return errors.InternalServer(reason.DatabaseError) + } + } + + //tag count + tagIDs := make([]string, 0) + Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID) + if tagerr != nil { + log.Error("GetObjectEntityTag error", tagerr) + return nil + } + for _, v := range Tags { + tagIDs = append(tagIDs, v.ID) + } + err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID) + if err != nil { + log.Error("RemoveTagRelListByObjectID error", err.Error()) + } + err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs) + if err != nil { + log.Error("efreshTagQuestionCount error", err.Error()) + } + + // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, + // facing the problem of recovery. + // err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) + // if err != nil { + // log.Errorf("user DeleteQuestion rank rollback error %s", err.Error()) + // } + err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: questionInfo.ID, + }) + if err != nil { + return + } + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: questionInfo.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionDeleted, + }) + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID).TID(questionInfo.ID). + QID(questionInfo.ID, questionInfo.UserID)) + return nil +} + +func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) { + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return + } + if !has { + return + } + + oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID) + if tagerr != nil { + log.Error("GetObjectEntityTag error", tagerr) + return nil, nil + } + + tagNameList := make([]string, 0) + oldtagNameList := make([]string, 0) + for _, tag := range req.Tags { + tagNameList = append(tagNameList, tag.SlugName) + } + for _, tag := range oldTags { + oldtagNameList = append(oldtagNameList, tag.SlugName) + } + + isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList) + + //If the content is the same, ignore it + if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange { + return + } + + Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + log.Error("GetTagListByNames error", tagerr) + return nil, nil + } + + // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added. + if !req.CanUseReservedTag { + CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags) + if !CheckOldTag { + errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`, + strings.Join(CheckOldTaglist, ",")) + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) + return errorlist, err + } + if !CheckNewTag { + errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, + strings.Join(CheckNewTaglist, ",")) + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) + return errorlist, err + } + } + return nil, nil +} + +func (qs *QuestionService) RecoverQuestion(ctx context.Context, req *schema.QuestionRecoverReq) (err error) { + questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + if questionInfo.Status != entity.QuestionStatusDeleted { + return nil + } + + err = qs.questionRepo.RecoverQuestion(ctx, req.QuestionID) + if err != nil { + return err + } + + // update user's question count + userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID) + if err != nil { + log.Error("user GetUserQuestionCount error", err.Error()) + } else { + err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) + if err != nil { + log.Error("user IncreaseQuestionCount error", err.Error()) + } + } + + // update tag's question count + if err = qs.tagCommon.RecoverTagRelListByObjectID(ctx, questionInfo.ID); err != nil { + log.Errorf("remove tag rel list by object id error %v", err) + } + + tagIDs := make([]string, 0) + tags, err := qs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID) + if err != nil { + return err + } + for _, v := range tags { + tagIDs = append(tagIDs, v.ID) + } + if len(tagIDs) > 0 { + if err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs); err != nil { + log.Errorf("update tag's question count failed, %v", err) + } + } + err = qs.questionRepo.RecoverQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + }, &entity.QuestionLink{ + ToQuestionID: questionInfo.ID, + }) + if err != nil { + return + } + + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionUndeleted, + }) + return nil +} + +func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) { + originQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + + //verify invite user + inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser) + if err != nil { + log.Error("BatchGetUserBasicInfoByUserNames error", err.Error()) + } + inviteUserIDs := make([]string, 0) + for _, item := range req.InviteUser { + _, ok := inviteUserInfoList[item] + if ok { + inviteUserIDs = append(inviteUserIDs, inviteUserInfoList[item].ID) + } + } + inviteUserStr := "" + inviteUserByte, err := json.Marshal(inviteUserIDs) + if err != nil { + log.Error("json.Marshal error", err.Error()) + inviteUserStr = "[]" + } else { + inviteUserStr = string(inviteUserByte) + } + question := &entity.Question{} + question.ID = uid.DeShortID(req.ID) + question.InviteUserID = inviteUserStr + + saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"}) + if saveerr != nil { + return saveerr + } + //send notification + oldInviteUserIDsStr := originQuestion.InviteUserID + oldInviteUserIDs := make([]string, 0) + needSendNotificationUserIDs := make([]string, 0) + if oldInviteUserIDsStr != "" { + err = json.Unmarshal([]byte(oldInviteUserIDsStr), &oldInviteUserIDs) + if err == nil { + needSendNotificationUserIDs = converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs) + } + } else { + needSendNotificationUserIDs = inviteUserIDs + } + go qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID) + + return nil +} + +func (qs *QuestionService) notificationInviteUser( + ctx context.Context, invitedUserIDs []string, questionID, questionTitle, questionUserID string) { + inviter, exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + users, err := qs.userRepo.BatchGetByID(ctx, invitedUserIDs) + if err != nil { + log.Error(err) + return + } + invitee := make(map[string]*entity.User, len(users)) + for _, user := range users { + invitee[user.ID] = user + } + for _, userID := range invitedUserIDs { + msg := &schema.NotificationMsg{ + ReceiverUserID: userID, + TriggerUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: questionID, + } + msg.ObjectType = constant.QuestionObjectType + msg.NotificationAction = constant.NotificationInvitedYouToAnswer + qs.notificationQueueService.Send(ctx, msg) + + receiverUserInfo, ok := invitee[userID] + if !ok { + log.Warnf("user %s not found", userID) + return + } + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewInviteAnswerTemplateRawData{ + InviterDisplayName: inviter.DisplayName, + QuestionTitle: questionTitle, + QuestionID: questionID, + UnsubscribeCode: token.GenerateToken(), + } + externalNotificationMsg.NewInviteAnswerTemplateRawData = rawData + qs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) + } +} + +// UpdateQuestion update question +func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) { + var canUpdate bool + questionInfo = &schema.QuestionInfoResp{} + + _, existUnreviewed, err := qs.revisionService.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return + } + if existUnreviewed { + err = errors.BadRequest(reason.QuestionCannotUpdate) + return + } + + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) + if err != nil { + return + } + if !has { + return + } + if dbinfo.Status == entity.QuestionStatusDeleted { + err = errors.BadRequest(reason.QuestionCannotUpdate) + return nil, err + } + + now := time.Now() + question := &entity.Question{} + question.Title = req.Title + question.OriginalText = req.Content + question.ParsedText = req.HTML + question.ID = uid.DeShortID(req.ID) + question.UpdatedAt = now + question.PostUpdateTime = now + question.UserID = dbinfo.UserID + question.LastEditUserID = req.UserID + + oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID) + if tagerr != nil { + return questionInfo, tagerr + } + + tagNameList := make([]string, 0) + oldtagNameList := make([]string, 0) + for _, tag := range req.Tags { + tag.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") + tagNameList = append(tagNameList, tag.SlugName) + } + for _, tag := range oldTags { + oldtagNameList = append(oldtagNameList, tag.SlugName) + } + + isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList) + + //If the content is the same, ignore it + if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange { + return + } + + Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList) + if tagerr != nil { + return questionInfo, tagerr + } + + // if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added. + if !req.CanUseReservedTag { + CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags) + if !CheckOldTag { + errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`, + strings.Join(CheckOldTaglist, ",")) + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) + return errorlist, err + } + if !CheckNewTag { + errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`, + strings.Join(CheckNewTaglist, ",")) + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: errMsg, + }) + err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg) + return errorlist, err + } + } + // Check whether mandatory labels are selected + recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) + if err != nil { + return + } + if !recommendExist { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "tags", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.RecommendTagEnter), + }) + err = errors.BadRequest(reason.RecommendTagEnter) + return errorlist, err + } + + //Administrators and themselves do not need to be audited + + revisionDTO := &schema.AddRevisionDTO{ + UserID: question.UserID, + ObjectID: question.ID, + Title: question.Title, + Log: req.EditSummary, + } + + if req.NoNeedReview { + canUpdate = true + } + + // It's not you or the administrator that needs to be reviewed + if !canUpdate { + revisionDTO.Status = entity.RevisionUnreviewedStatus + revisionDTO.UserID = req.UserID //use revision userid + } else { + //Direct modification + revisionDTO.Status = entity.RevisionReviewPassStatus + //update question to db + question.ParsedText, err = qs.questioncommon.UpdateQuestionLink(ctx, question.ID, "", question.ParsedText, question.OriginalText) + if err != nil { + return questionInfo, err + } + saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) + if saveerr != nil { + return questionInfo, saveerr + } + objectTagData := schema.TagChange{} + objectTagData.ObjectID = question.ID + objectTagData.Tags = req.Tags + objectTagData.UserID = req.UserID + tagerr := qs.ChangeTag(ctx, &objectTagData) + if tagerr != nil { + return questionInfo, tagerr + } + } + + questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags) + if err != nil { + return nil, err + } + infoJSON, _ := json.Marshal(questionWithTagsRevision) + revisionDTO.Content = string(infoJSON) + revisionID, err := qs.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return + } + if canUpdate { + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: question.ID, + ActivityTypeKey: constant.ActQuestionEdited, + RevisionID: revisionID, + OriginalObjectID: question.ID, + }) + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID).TID(question.ID). + QID(question.ID, question.UserID)) + } + + questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) + return +} + +// GetQuestion get question one +func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID string, + per schema.QuestionPermission) (resp *schema.QuestionInfoResp, err error) { + question, err := qs.questioncommon.Info(ctx, questionID, userID) + if err != nil { + return + } + // If the question is deleted or pending, only the administrator and the author can view it + if (question.Status == entity.QuestionStatusDeleted || + question.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID { + return nil, errors.NotFound(reason.QuestionNotFound) + } + if question.Status != entity.QuestionStatusClosed { + per.CanReopen = false + } + if question.Status == entity.QuestionStatusClosed { + per.CanClose = false + } + if question.Pin == entity.QuestionPin { + per.CanPin = false + per.CanHide = false + } + if question.Pin == entity.QuestionUnPin { + per.CanUnPin = false + } + if question.Show == entity.QuestionShow { + per.CanShow = false + } + if question.Show == entity.QuestionHide { + per.CanHide = false + per.CanPin = false + } + + if question.Status == entity.QuestionStatusDeleted { + operation := &schema.Operation{} + operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted) + operation.Level = schema.OperationLevelDanger + question.Operation = operation + } + if question.Status == entity.QuestionStatusPending { + operation := &schema.Operation{} + operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionUnderReview) + operation.Level = schema.OperationLevelSecondary + question.Operation = operation + } + + question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240) + question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID, question.Status, + per.CanEdit, per.CanDelete, + per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow, + per.CanRecover) + question.ExtendsActions = permission.GetQuestionExtendsPermission(ctx, per.CanInviteOtherToAnswer) + return question, nil +} + +// GetQuestionAndAddPV get question one +func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID, loginUserID string, + per schema.QuestionPermission) ( + resp *schema.QuestionInfoResp, err error) { + err = qs.questioncommon.UpdatePv(ctx, questionID) + if err != nil { + log.Error(err) + } + return qs.GetQuestion(ctx, questionID, loginUserID, per) +} + +func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { + return qs.questioncommon.InviteUserInfo(ctx, questionID) +} + +func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error { + return qs.tagCommon.ObjectChangeTag(ctx, objectTagData) +} + +func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) { + return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData) +} + +// PersonalQuestionPage get question list by user +func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) ( + pageModel *pager.PageModel, err error) { + + userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + search := &schema.QuestionPageReq{} + search.OrderCond = req.OrderCond + search.Page = req.Page + search.PageSize = req.PageSize + search.UserIDBeSearched = userinfo.ID + search.LoginUserID = req.LoginUserID + // Only author and administrator can view the pending question + if req.LoginUserID == userinfo.ID || req.IsAdmin { + search.ShowPending = true + } + questionList, total, err := qs.GetQuestionPage(ctx, search) + if err != nil { + return nil, err + } + userQuestionInfoList := make([]*schema.UserQuestionInfo, 0) + for _, item := range questionList { + info := &schema.UserQuestionInfo{} + _ = copier.Copy(info, item) + status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status] + if ok { + info.Status = status + } + userQuestionInfoList = append(userQuestionInfoList, info) + } + return pager.NewPageModel(total, userQuestionInfoList), nil +} + +func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) ( + pageModel *pager.PageModel, err error) { + userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + cond := &entity.PersonalAnswerPageQueryCond{} + cond.UserID = userinfo.ID + cond.Page = req.Page + cond.PageSize = req.PageSize + cond.ShowPending = req.IsAdmin || req.LoginUserID == cond.UserID + if req.OrderCond == "newest" { + cond.Order = entity.AnswerSearchOrderByTime + } else { + cond.Order = entity.AnswerSearchOrderByDefault + } + questionIDs := make([]string, 0) + answerList, total, err := qs.questioncommon.AnswerCommon.PersonalAnswerPage(ctx, cond) + if err != nil { + return nil, err + } + + answerlist := make([]*schema.AnswerInfo, 0) + userAnswerlist := make([]*schema.UserAnswerInfo, 0) + for _, item := range answerList { + answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) + answerlist = append(answerlist, answerinfo) + questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID)) + } + questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID) + if err != nil { + return nil, err + } + + for _, item := range answerlist { + _, ok := questionMaps[item.QuestionID] + if ok { + item.QuestionInfo = questionMaps[item.QuestionID] + } else { + continue + } + info := &schema.UserAnswerInfo{} + _ = copier.Copy(info, item) + info.AnswerID = item.ID + info.QuestionID = item.QuestionID + if item.QuestionInfo.Status == entity.QuestionStatusDeleted { + info.QuestionInfo.Title = "Deleted question" + + } + userAnswerlist = append(userAnswerlist, info) + } + + return pager.NewPageModel(total, userAnswerlist), nil +} + +// PersonalCollectionPage get collection list by user +func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) ( + pageModel *pager.PageModel, err error) { + list := make([]*schema.QuestionInfoResp, 0) + collectionSearch := &entity.CollectionSearch{} + collectionSearch.UserID = req.UserID + collectionSearch.Page = req.Page + collectionSearch.PageSize = req.PageSize + collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch) + if err != nil { + return nil, err + } + questionIDs := make([]string, 0) + for _, item := range collectionList { + questionIDs = append(questionIDs, item.ObjectID) + } + + questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID) + if err != nil { + return nil, err + } + for _, id := range questionIDs { + if handler.GetEnableShortID(ctx) { + id = uid.EnShortID(id) + } + _, ok := questionMaps[id] + if ok { + questionMaps[id].LastAnsweredUserInfo = nil + questionMaps[id].UpdateUserInfo = nil + questionMaps[id].Content = "" + questionMaps[id].HTML = "" + if questionMaps[id].Status == entity.QuestionStatusDeleted { + questionMaps[id].Title = "Deleted question" + } + list = append(list, questionMaps[id]) + } + } + + return pager.NewPageModel(total, list), nil +} + +func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) { + answerlist := make([]*schema.AnswerInfo, 0) + + userAnswerlist := make([]*schema.UserAnswerInfo, 0) + userQuestionlist := make([]*schema.UserQuestionInfo, 0) + + userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName) + if err != nil { + return userQuestionlist, userAnswerlist, err + } + if !Exist { + return userQuestionlist, userAnswerlist, nil + } + search := &schema.QuestionPageReq{} + search.OrderCond = "score" + search.Page = 0 + search.PageSize = 5 + search.UserIDBeSearched = userinfo.ID + search.LoginUserID = loginUserID + questionlist, _, err := qs.GetQuestionPage(ctx, search) + if err != nil { + return userQuestionlist, userAnswerlist, err + } + answersearch := &entity.AnswerSearch{} + answersearch.UserID = userinfo.ID + answersearch.PageSize = 5 + answersearch.Order = entity.AnswerSearchOrderByVote + questionIDs := make([]string, 0) + answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) + if err != nil { + return userQuestionlist, userAnswerlist, err + } + for _, item := range answerList { + answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) + answerlist = append(answerlist, answerinfo) + questionIDs = append(questionIDs, item.QuestionID) + } + questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) + if err != nil { + return userQuestionlist, userAnswerlist, err + } + for _, item := range answerlist { + _, ok := questionMaps[item.QuestionID] + if ok { + item.QuestionInfo = questionMaps[item.QuestionID] + } + } + + for _, item := range questionlist { + info := &schema.UserQuestionInfo{} + _ = copier.Copy(info, item) + info.UrlTitle = htmltext.UrlTitle(info.Title) + userQuestionlist = append(userQuestionlist, info) + } + + for _, item := range answerlist { + info := &schema.UserAnswerInfo{} + _ = copier.Copy(info, item) + info.AnswerID = item.ID + info.QuestionID = item.QuestionID + info.QuestionInfo.UrlTitle = htmltext.UrlTitle(info.QuestionInfo.Title) + userAnswerlist = append(userAnswerlist, info) + } + + return userQuestionlist, userAnswerlist, nil +} + +// GetQuestionsByTitle get questions by title +func (qs *QuestionService) GetQuestionsByTitle(ctx context.Context, title string) ( + resp []*schema.QuestionBaseInfo, err error) { + resp = make([]*schema.QuestionBaseInfo, 0) + if len(title) == 0 { + return resp, nil + } + // check search plugin + var finder plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + finder = search + return nil + }) + + var questions []*entity.Question + if finder != nil { + // call search plugin if available + words := []string{title} + res, _, err := finder.SearchQuestions(ctx, &plugin.SearchBasicCond{ + Words: words, + Page: 1, + PageSize: 10, + }) + if err != nil { + return resp, err + } + // get question ids from res + questionIDs := make([]string, 0) + for _, question := range res { + questionIDs = append(questionIDs, question.ID) + } + questions, err = qs.questionRepo.FindByID(ctx, questionIDs) + } else { + questions, err = qs.questionRepo.GetQuestionsByTitle(ctx, title, 10) + } + + if err != nil { + return resp, err + } + for _, question := range questions { + item := &schema.QuestionBaseInfo{} + item.ID = question.ID + item.Title = question.Title + item.UrlTitle = htmltext.UrlTitle(question.Title) + item.ViewCount = question.ViewCount + item.AnswerCount = question.AnswerCount + item.CollectionCount = question.CollectionCount + item.FollowCount = question.FollowCount + status, ok := entity.AdminQuestionSearchStatusIntToString[question.Status] + if ok { + item.Status = status + } + if question.AcceptedAnswerID != "0" { + item.AcceptedAnswer = true + } + resp = append(resp, item) + } + return resp, nil +} + +// SimilarQuestion +func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionPageResp, int64, error) { + question, err := qs.questioncommon.Info(ctx, questionID, loginUserID) + if err != nil { + return nil, 0, nil + } + tagNames := make([]string, 0, len(question.Tags)) + for _, tag := range question.Tags { + tagNames = append(tagNames, tag.SlugName) + } + search := &schema.QuestionPageReq{} + search.OrderCond = "hot" + search.Page = 0 + search.PageSize = 6 + if len(tagNames) > 0 { + search.Tag = tagNames[0] + } + search.LoginUserID = loginUserID + similarQuestions, _, err := qs.GetQuestionPage(ctx, search) + if err != nil { + return nil, 0, err + } + var result []*schema.QuestionPageResp + for _, v := range similarQuestions { + if uid.DeShortID(v.ID) != questionID { + result = append(result, v) + } + } + return result, int64(len(result)), nil +} + +// GetQuestionPage query questions page +func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( + questions []*schema.QuestionPageResp, total int64, err error) { + questions = make([]*schema.QuestionPageResp, 0) + // query by user role + showHidden := false + if req.LoginUserID != "" && req.UserIDBeSearched != "" { + showHidden = req.LoginUserID == req.UserIDBeSearched + if !showHidden { + userRole, err := qs.userRoleRelService.GetUserRole(ctx, req.LoginUserID) + if err != nil { + return nil, 0, err + } + showHidden = userRole == role.RoleAdminID || userRole == role.RoleModeratorID + } + } + // query by tag condition + var tagIDs = make([]string, 0) + if len(req.Tag) > 0 { + tagInfo, exist, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag)) + if err != nil { + return nil, 0, err + } + if exist { + synTagIds, err := qs.tagCommon.GetTagIDsByMainTagID(ctx, tagInfo.ID) + if err != nil { + return nil, 0, err + } + tagIDs = append(synTagIds, tagInfo.ID) + } else { + return questions, 0, nil + } + } + + // query by user condition + if req.Username != "" { + userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) + if err != nil { + return nil, 0, err + } + if !exist { + return questions, 0, nil + } + req.UserIDBeSearched = userinfo.ID + } + + if req.OrderCond == schema.QuestionOrderCondHot { + req.InDays = schema.HotInDays + } + + questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize, + tagIDs, req.UserIDBeSearched, req.OrderCond, req.InDays, showHidden, req.ShowPending) + if err != nil { + return nil, 0, err + } + questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond) + if err != nil { + return nil, 0, err + } + return questions, total, nil +} + +// GetRecommendQuestionPage retrieves recommended question page based on following tags and questions. +func (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( + questions []*schema.QuestionPageResp, total int64, err error) { + followingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID) + if err != nil { + return nil, 0, err + } + tagIDs := make([]string, 0, len(followingTagsResp)) + for _, tag := range followingTagsResp { + tagIDs = append(tagIDs, tag.TagID) + } + + activityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, "follow") + if err != nil { + return nil, 0, err + } + activities, err := qs.activityRepo.GetUserActivitiesByActivityType(ctx, req.LoginUserID, activityType) + if err != nil { + return nil, 0, err + } + + followedQuestionIDs := make([]string, 0, len(activities)) + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + followedQuestionIDs = append(followedQuestionIDs, activity.ObjectID) + } + questionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize) + if err != nil { + return nil, 0, err + } + + questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, schema.QuestionOrderCondFrequent) + if err != nil { + return nil, 0, err + } + + return questions, total, nil +} + +func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error { + setStatus, ok := entity.AdminQuestionSearchStatus[req.Status] + if !ok { + return errors.BadRequest(reason.RequestFormatError) + } + questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, req.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, setStatus) + if err != nil { + return err + } + + msg := &schema.NotificationMsg{} + if setStatus == entity.QuestionStatusDeleted { + // #2372 In order to simplify the process and complexity, as well as to consider if it is in-house, + // facing the problem of recovery. + //err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) + //if err != nil { + // log.Errorf("admin delete question then rank rollback error %s", err.Error()) + //} + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: questionInfo.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionDeleted, + }) + msg.NotificationAction = constant.NotificationYourQuestionWasDeleted + } + if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed { + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: questionInfo.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionReopened, + }) + } + if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed { + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: questionInfo.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) + msg.NotificationAction = constant.NotificationYourQuestionIsClosed + } + // recover + if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusDeleted { + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionUndeleted, + }) + } + + if len(msg.NotificationAction) > 0 { + msg.ObjectID = questionInfo.ID + msg.Type = schema.NotificationTypeInbox + msg.ReceiverUserID = questionInfo.UserID + msg.TriggerUserID = req.UserID + msg.ObjectType = constant.QuestionObjectType + qs.notificationQueueService.Send(ctx, msg) + } + return nil +} + +func (qs *QuestionService) AdminQuestionPage( + ctx context.Context, req *schema.AdminQuestionPageReq) ( + resp *pager.PageModel, err error) { + + list := make([]*schema.AdminQuestionInfo, 0) + questionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req) + if err != nil { + return nil, err + } + + userIds := make([]string, 0) + for _, info := range questionList { + item := &schema.AdminQuestionInfo{} + _ = copier.Copy(item, info) + item.CreateTime = info.CreatedAt.Unix() + item.UpdateTime = info.PostUpdateTime.Unix() + item.EditTime = info.UpdatedAt.Unix() + list = append(list, item) + userIds = append(userIds, info.UserID) + } + userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) + if err != nil { + return nil, err + } + for _, item := range list { + if u, ok := userInfoMap[item.UserID]; ok { + item.UserInfo = u + } + } + return pager.NewPageModel(count, list), nil +} + +// AdminAnswerPage search answer list +func (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) ( + resp *pager.PageModel, err error) { + answerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req) + if err != nil { + return nil, err + } + + questionIDs := make([]string, 0) + userIds := make([]string, 0) + answerResp := make([]*schema.AdminAnswerInfo, 0) + for _, item := range answerList { + answerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item) + answerResp = append(answerResp, answerInfo) + questionIDs = append(questionIDs, item.QuestionID) + userIds = append(userIds, item.UserID) + } + userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) + if err != nil { + return nil, err + } + questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID) + if err != nil { + return nil, err + } + + for _, item := range answerResp { + if q, ok := questionMaps[item.QuestionID]; ok { + item.QuestionInfo.Title = q.Title + } + if u, ok := userInfoMap[item.UserID]; ok { + item.UserInfo = u + } + } + return pager.NewPageModel(count, answerResp), nil +} + +func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questionInfo *entity.Question, tags []*entity.Tag) ( + questionRevision *entity.QuestionWithTagsRevision, err error) { + questionRevision = &entity.QuestionWithTagsRevision{} + questionRevision.Question = *questionInfo + + for _, tag := range tags { + item := &entity.TagSimpleInfoForRevision{} + _ = copier.Copy(item, tag) + questionRevision.Tags = append(questionRevision.Tags, item) + } + return questionRevision, nil +} + +func (qs *QuestionService) SitemapCron(ctx context.Context) { + siteSeo, err := qs.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return + } + ctx = context.WithValue(ctx, constant.ShortIDFlag, siteSeo.IsShortLink()) + qs.questioncommon.SitemapCron(ctx) +} + +func (qs *QuestionService) GetQuestionLink(ctx context.Context, req *schema.GetQuestionLinkReq) ( + questions []*schema.QuestionPageResp, total int64, err error) { + if req.OrderCond == schema.QuestionOrderCondHot { + req.InDays = schema.HotInDays + } + + questionList, total, err := qs.questionRepo.GetQuestionLink(ctx, req.Page, req.PageSize, req.QuestionID, req.OrderCond, req.InDays) + if err != nil { + return nil, 0, err + } + + questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, req.OrderCond) + if err != nil { + return nil, 0, err + } + return questions, total, nil +} diff --git a/internal/service/content/revision_service.go b/internal/service/content/revision_service.go new file mode 100644 index 000000000..effc012ba --- /dev/null +++ b/internal/service/content/revision_service.go @@ -0,0 +1,541 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + "encoding/json" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_queue" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/object_info" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/report_common" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision" + "github.com/apache/answer/internal/service/tag_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// RevisionService user service +type RevisionService struct { + revisionRepo revision.RevisionRepo + userCommon *usercommon.UserCommon + questionCommon *questioncommon.QuestionCommon + answerService *AnswerService + objectInfoService *object_info.ObjService + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + tagRepo tag_common.TagRepo + tagCommon *tagcommon.TagCommonService + notificationQueueService notice_queue.NotificationQueueService + activityQueueService activity_queue.ActivityQueueService + reportRepo report_common.ReportRepo + reviewService *review.ReviewService + reviewActivity activity.ReviewActivityRepo +} + +func NewRevisionService( + revisionRepo revision.RevisionRepo, + userCommon *usercommon.UserCommon, + questionCommon *questioncommon.QuestionCommon, + answerService *AnswerService, + objectInfoService *object_info.ObjService, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + tagRepo tag_common.TagRepo, + tagCommon *tagcommon.TagCommonService, + notificationQueueService notice_queue.NotificationQueueService, + activityQueueService activity_queue.ActivityQueueService, + reportRepo report_common.ReportRepo, + reviewService *review.ReviewService, + reviewActivity activity.ReviewActivityRepo, +) *RevisionService { + return &RevisionService{ + revisionRepo: revisionRepo, + userCommon: userCommon, + questionCommon: questionCommon, + answerService: answerService, + objectInfoService: objectInfoService, + questionRepo: questionRepo, + answerRepo: answerRepo, + tagRepo: tagRepo, + tagCommon: tagCommon, + notificationQueueService: notificationQueueService, + activityQueueService: activityQueueService, + reportRepo: reportRepo, + reviewService: reviewService, + reviewActivity: reviewActivity, + } +} + +func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.RevisionAuditReq) (err error) { + revisioninfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, req.ID) + if err != nil { + return + } + if !exist { + return + } + if revisioninfo.Status != entity.RevisionUnreviewedStatus { + return + } + if req.Operation == schema.RevisionAuditReject { + err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID) + return + } + if req.Operation == schema.RevisionAuditApprove { + objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) + if objectTypeerr != nil { + return objectTypeerr + } + revisionitem := &schema.GetRevisionResp{} + _ = copier.Copy(revisionitem, revisioninfo) + rs.parseItem(ctx, revisionitem) + var saveErr error + switch objectType { + case constant.QuestionObjectType: + if !req.CanReviewQuestion { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditQuestion(ctx, revisionitem) + } + case constant.AnswerObjectType: + if !req.CanReviewAnswer { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditAnswer(ctx, revisionitem) + } + case constant.TagObjectType: + if !req.CanReviewTag { + saveErr = errors.BadRequest(reason.RevisionNoPermission) + } else { + saveErr = rs.revisionAuditTag(ctx, revisionitem) + } + } + if saveErr != nil { + return saveErr + } + err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewPassStatus, req.UserID) + if err != nil { + return err + } + err = rs.reviewActivity.Review(ctx, &schema.PassReviewActivity{ + UserID: revisioninfo.UserID, + TriggerUserID: req.UserID, + ObjectID: revisioninfo.ObjectID, + OriginalObjectID: "0", + RevisionID: revisioninfo.ID, + }) + if err != nil { + log.Errorf("add review activity failed: %v", err) + } + + msg := &schema.NotificationMsg{ + TriggerUserID: req.UserID, + ReceiverUserID: revisioninfo.UserID, + Type: schema.NotificationTypeAchievement, + ObjectID: revisioninfo.ObjectID, + ObjectType: objectType, + } + rs.notificationQueueService.Send(ctx, msg) + return + } + + return nil +} + +func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp) + if ok { + var PostUpdateTime time.Time + dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, questioninfo.ID) + if dberr != nil || !exist { + return + } + + PostUpdateTime = time.Unix(questioninfo.UpdateTime, 0) + if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { + PostUpdateTime = dbquestion.PostUpdateTime + } + question := &entity.Question{} + question.ID = questioninfo.ID + question.Title = questioninfo.Title + question.OriginalText = questioninfo.Content + question.ParsedText = questioninfo.HTML + question.UpdatedAt = time.Unix(questioninfo.UpdateTime, 0) + question.PostUpdateTime = PostUpdateTime + question.LastEditUserID = revisionitem.UserID + saveerr := rs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at", "post_update_time", "last_edit_user_id"}) + if saveerr != nil { + return saveerr + } + objectTagTags := make([]*schema.TagItem, 0) + for _, tag := range questioninfo.Tags { + item := &schema.TagItem{} + item.SlugName = tag.SlugName + objectTagTags = append(objectTagTags, item) + } + objectTagData := schema.TagChange{} + objectTagData.ObjectID = question.ID + objectTagData.Tags = objectTagTags + saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData) + if saveerr != nil { + return saveerr + } + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: revisionitem.ObjectID, + ActivityTypeKey: constant.ActQuestionEdited, + RevisionID: revisionitem.ID, + OriginalObjectID: revisionitem.ObjectID, + }) + } + return nil +} + +func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + answerinfo, ok := revisionitem.ContentParsed.(*schema.AnswerInfo) + if ok { + + var PostUpdateTime time.Time + dbquestion, exist, dberr := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) + if dberr != nil || !exist { + return + } + + PostUpdateTime = time.Unix(answerinfo.UpdateTime, 0) + if dbquestion.PostUpdateTime.Unix() > PostUpdateTime.Unix() { + PostUpdateTime = dbquestion.PostUpdateTime + } + + insertData := new(entity.Answer) + insertData.ID = answerinfo.ID + insertData.OriginalText = answerinfo.Content + insertData.ParsedText = answerinfo.HTML + insertData.UpdatedAt = time.Unix(answerinfo.UpdateTime, 0) + insertData.LastEditUserID = revisionitem.UserID + saveerr := rs.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "updated_at", "last_edit_user_id"}) + if saveerr != nil { + return saveerr + } + saveerr = rs.questionCommon.UpdatePostSetTime(ctx, answerinfo.QuestionID, PostUpdateTime) + if saveerr != nil { + return saveerr + } + questionInfo, exist, err := rs.questionRepo.GetQuestion(ctx, answerinfo.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.QuestionNotFound) + } + msg := &schema.NotificationMsg{ + TriggerUserID: revisionitem.UserID, + ReceiverUserID: questionInfo.UserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerinfo.ID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationUpdateAnswer + rs.notificationQueueService.Send(ctx, msg) + + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: insertData.ID, + OriginalObjectID: insertData.ID, + ActivityTypeKey: constant.ActAnswerEdited, + RevisionID: revisionitem.ID, + }) + } + return nil +} + +func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { + taginfo, ok := revisionitem.ContentParsed.(*schema.GetTagResp) + if ok { + tag := &entity.Tag{} + tag.ID = taginfo.TagID + tag.OriginalText = taginfo.OriginalText + tag.ParsedText = taginfo.ParsedText + saveerr := rs.tagRepo.UpdateTag(ctx, tag) + if saveerr != nil { + return saveerr + } + + tagInfo, exist, err := rs.tagCommon.GetTagByID(ctx, taginfo.TagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + if tagInfo.MainTagID == 0 && len(tagInfo.SlugName) > 0 { + log.Debugf("tag %s update slug_name", tagInfo.SlugName) + tagList, err := rs.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) + if err != nil { + return err + } + updateTagSlugNames := make([]string, 0) + for _, tag := range tagList { + updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) + } + err = rs.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) + if err != nil { + return err + } + } + + rs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: revisionitem.UserID, + ObjectID: taginfo.TagID, + OriginalObjectID: taginfo.TagID, + ActivityTypeKey: constant.ActTagEdited, + RevisionID: revisionitem.ID, + }) + } + return nil +} + +// GetUnreviewedRevisionPage get unreviewed list +func (rs *RevisionService) GetUnreviewedRevisionPage(ctx context.Context, req *schema.RevisionSearch) ( + resp *pager.PageModel, err error) { + revisionResp := make([]*schema.GetUnreviewedRevisionResp, 0) + if len(req.GetCanReviewObjectTypes()) == 0 { + return pager.NewPageModel(0, revisionResp), nil + } + revisionPage, total, err := rs.revisionRepo.GetUnreviewedRevisionPage( + ctx, req.Page, 1, req.GetCanReviewObjectTypes()) + if err != nil { + return nil, err + } + for _, rev := range revisionPage { + item := &schema.GetUnreviewedRevisionResp{} + _, ok := constant.ObjectTypeNumberMapping[rev.ObjectType] + if !ok { + continue + } + item.Type = constant.ObjectTypeNumberMapping[rev.ObjectType] + info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, rev.ObjectID) + if err != nil { + return nil, err + } + item.Info = info + revisionitem := &schema.GetRevisionResp{} + _ = copier.Copy(revisionitem, rev) + rs.parseItem(ctx, revisionitem) + item.UnreviewedInfo = revisionitem + + // get user info + userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, revisionitem.UserID) + if e != nil { + return nil, e + } + if exists { + var uinfo schema.UserBasicInfo + _ = copier.Copy(&uinfo, userInfo) + item.UnreviewedInfo.UserInfo = uinfo + } + item.Info.UrlTitle = htmltext.UrlTitle(item.Info.Title) + item.UnreviewedInfo.UrlTitle = htmltext.UrlTitle(item.UnreviewedInfo.Title) + revisionResp = append(revisionResp, item) + } + return pager.NewPageModel(total, revisionResp), nil +} + +// GetRevisionList get revision list all +func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { + var ( + rev entity.Revision + revs []entity.Revision + ) + + resp = []schema.GetRevisionResp{} + _ = copier.Copy(&rev, req) + + revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev) + if err != nil { + return + } + + for _, r := range revs { + var ( + uinfo schema.UserBasicInfo + item schema.GetRevisionResp + ) + + _ = copier.Copy(&item, r) + rs.parseItem(ctx, &item) + + // get user info + userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID) + if e != nil { + return nil, e + } + if exists { + err = copier.Copy(&uinfo, userInfo) + item.UserInfo = uinfo + } + resp = append(resp, item) + } + return +} + +func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) { + var ( + err error + question entity.QuestionWithTagsRevision + questionInfo *schema.QuestionInfoResp + answer entity.Answer + answerInfo *schema.AnswerInfo + tag entity.Tag + tagInfo *schema.GetTagResp + ) + + shortID := handler.GetEnableShortID(ctx) + if shortID { + item.ObjectID = uid.EnShortID(item.ObjectID) + } + switch item.ObjectType { + case constant.ObjectTypeStrMapping["question"]: + err = json.Unmarshal([]byte(item.Content), &question) + if err != nil { + break + } + questionInfo = rs.questionCommon.ShowFormatWithTag(ctx, &question) + if shortID { + questionInfo.ID = uid.EnShortID(questionInfo.ID) + } + item.ContentParsed = questionInfo + case constant.ObjectTypeStrMapping["answer"]: + err = json.Unmarshal([]byte(item.Content), &answer) + if err != nil { + break + } + answerInfo = rs.answerService.ShowFormat(ctx, &answer) + if shortID { + answerInfo.ID = uid.EnShortID(answerInfo.ID) + answerInfo.QuestionID = uid.EnShortID(answerInfo.QuestionID) + } + item.ContentParsed = answerInfo + case constant.ObjectTypeStrMapping["tag"]: + err = json.Unmarshal([]byte(item.Content), &tag) + if err != nil { + break + } + tagInfo = &schema.GetTagResp{ + TagID: tag.ID, + CreatedAt: tag.CreatedAt.Unix(), + UpdatedAt: tag.UpdatedAt.Unix(), + SlugName: tag.SlugName, + DisplayName: tag.DisplayName, + OriginalText: tag.OriginalText, + ParsedText: tag.ParsedText, + FollowCount: tag.FollowCount, + QuestionCount: tag.QuestionCount, + Recommend: tag.Recommend, + Reserved: tag.Reserved, + } + tagInfo.GetExcerpt() + item.ContentParsed = tagInfo + } + + if err != nil { + item.ContentParsed = item.Content + } + item.CreatedAtParsed = item.CreatedAt.Unix() +} + +// CheckCanUpdateRevision can check revision +func (rs *RevisionService) CheckCanUpdateRevision(ctx context.Context, req *schema.CheckCanQuestionUpdate) ( + resp *schema.ErrTypeData, err error) { + _, exist, err := rs.revisionRepo.ExistUnreviewedByObjectID(ctx, req.ID) + if err != nil { + return nil, nil + } + if exist { + return &schema.ErrTypeToast, errors.BadRequest(reason.RevisionReviewUnderway) + } + return nil, nil +} + +// GetReviewingType get reviewing type +func (rs *RevisionService) GetReviewingType(ctx context.Context, req *schema.GetReviewingTypeReq) (resp []*schema.GetReviewingTypeResp, err error) { + resp = make([]*schema.GetReviewingTypeResp, 0) + + // get queue amount + if req.IsAdmin { + reviewCount, err := rs.reviewService.GetReviewPendingCount(ctx) + if err != nil { + log.Errorf("get report count failed: %v", err) + } else { + resp = append(resp, &schema.GetReviewingTypeResp{ + Name: string(constant.QueuedPost), + Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewQueuedPostLabel), + TodoAmount: reviewCount, + }) + } + } + + // get flag amount + if req.IsAdmin { + reportCount, err := rs.reportRepo.GetReportCount(ctx) + if err != nil { + log.Errorf("get report count failed: %v", err) + } else { + resp = append(resp, &schema.GetReviewingTypeResp{ + Name: string(constant.FlaggedPost), + Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewFlaggedPostLabel), + TodoAmount: reportCount, + }) + } + } + + // get suggestion amount + countUnreviewedRevision, err := rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) + if err != nil { + log.Errorf("get unreviewed revision count failed: %v", err) + } else { + resp = append(resp, &schema.GetReviewingTypeResp{ + Name: string(constant.SuggestedPostEdit), + Label: translator.Tr(handler.GetLangByCtx(ctx), constant.ReviewSuggestedPostEditLabel), + TodoAmount: countUnreviewedRevision, + }) + } + return resp, nil +} diff --git a/internal/service/content/search_service.go b/internal/service/content/search_service.go new file mode 100644 index 000000000..98add0938 --- /dev/null +++ b/internal/service/content/search_service.go @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/search_common" + "github.com/apache/answer/internal/service/search_parser" + "github.com/apache/answer/plugin" +) + +type SearchService struct { + searchParser *search_parser.SearchParser + searchRepo search_common.SearchRepo +} + +func NewSearchService( + searchParser *search_parser.SearchParser, + searchRepo search_common.SearchRepo, +) *SearchService { + return &SearchService{ + searchParser: searchParser, + searchRepo: searchRepo, + } +} + +// Search search contents +func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) { + if dto.Page < 1 { + dto.Page = 1 + } + if len(dto.Query) == 0 { + return &schema.SearchResp{ + Total: 0, + SearchResults: make([]*schema.SearchResult, 0), + }, nil + } + + // search type + cond := ss.searchParser.ParseStructure(ctx, dto) + + // check search plugin + var finder plugin.Search + _ = plugin.CallSearch(func(search plugin.Search) error { + finder = search + return nil + }) + + resp = &schema.SearchResp{} + // search plugin is not found, call system search + if finder == nil { + if cond.SearchAll() { + resp.SearchResults, resp.Total, err = + ss.searchRepo.SearchContents(ctx, cond.Words, cond.Tags, cond.UserID, cond.VoteAmount, dto.Page, dto.Size, dto.Order) + } else if cond.SearchQuestion() { + resp.SearchResults, resp.Total, err = + ss.searchRepo.SearchQuestions(ctx, cond.Words, cond.Tags, cond.NotAccepted, cond.Views, cond.AnswerAmount, dto.Page, dto.Size, dto.Order) + } else if cond.SearchAnswer() { + resp.SearchResults, resp.Total, err = + ss.searchRepo.SearchAnswers(ctx, cond.Words, cond.Tags, cond.Accepted, cond.QuestionID, dto.Page, dto.Size, dto.Order) + } + return + } + return ss.searchByPlugin(ctx, finder, cond, dto) +} + +func (ss *SearchService) searchByPlugin(ctx context.Context, finder plugin.Search, cond *schema.SearchCondition, dto *schema.SearchDTO) (resp *schema.SearchResp, err error) { + var res []plugin.SearchResult + resp = &schema.SearchResp{} + if cond.SearchAll() { + res, resp.Total, err = finder.SearchContents(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } else if cond.SearchQuestion() { + res, resp.Total, err = finder.SearchQuestions(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } else if cond.SearchAnswer() { + res, resp.Total, err = finder.SearchAnswers(ctx, cond.Convert2PluginSearchCond(dto.Page, dto.Size, dto.Order)) + } + + resp.SearchResults, err = ss.searchRepo.ParseSearchPluginResult(ctx, res, cond.Words) + return resp, err +} diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go new file mode 100644 index 000000000..81f7ac824 --- /dev/null +++ b/internal/service/content/user_service.go @@ -0,0 +1,985 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/apache/answer/internal/service/event_queue" + "github.com/apache/answer/pkg/token" + + "github.com/apache/answer/internal/base/constant" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/user_notification_config" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/file_record" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "golang.org/x/crypto/bcrypt" +) + +// UserService user service +type UserService struct { + userCommonService *usercommon.UserCommon + userRepo usercommon.UserRepo + userActivity activity.UserActiveActivityRepo + activityRepo activity_common.ActivityRepo + emailService *export.EmailService + authService *auth.AuthService + siteInfoService siteinfo_common.SiteInfoCommonService + userRoleService *role.UserRoleRelService + userExternalLoginService *user_external_login.UserExternalLoginService + userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo + userNotificationConfigService *user_notification_config.UserNotificationConfigService + questionService *questioncommon.QuestionCommon + eventQueueService event_queue.EventQueueService + fileRecordService *file_record.FileRecordService +} + +func NewUserService(userRepo usercommon.UserRepo, + userActivity activity.UserActiveActivityRepo, + activityRepo activity_common.ActivityRepo, + emailService *export.EmailService, + authService *auth.AuthService, + siteInfoService siteinfo_common.SiteInfoCommonService, + userRoleService *role.UserRoleRelService, + userCommonService *usercommon.UserCommon, + userExternalLoginService *user_external_login.UserExternalLoginService, + userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, + userNotificationConfigService *user_notification_config.UserNotificationConfigService, + questionService *questioncommon.QuestionCommon, + eventQueueService event_queue.EventQueueService, + fileRecordService *file_record.FileRecordService, +) *UserService { + return &UserService{ + userCommonService: userCommonService, + userRepo: userRepo, + userActivity: userActivity, + activityRepo: activityRepo, + emailService: emailService, + authService: authService, + siteInfoService: siteInfoService, + userRoleService: userRoleService, + userExternalLoginService: userExternalLoginService, + userNotificationConfigRepo: userNotificationConfigRepo, + userNotificationConfigService: userNotificationConfigService, + questionService: questionService, + eventQueueService: eventQueueService, + fileRecordService: fileRecordService, + } +} + +// GetUserInfoByUserID get user info by user id +func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) ( + resp *schema.GetCurrentLoginUserInfoResp, err error) { + userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + if userInfo.Status == entity.UserStatusDeleted { + return nil, errors.Unauthorized(reason.UnauthorizedError) + } + + resp = &schema.GetCurrentLoginUserInfoResp{} + resp.ConvertFromUserEntity(userInfo) + resp.RoleID, err = us.userRoleService.GetUserRole(ctx, userInfo.ID) + if err != nil { + log.Error(err) + } + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status) + resp.AccessToken = token + resp.HavePassword = len(userInfo.Pass) > 0 + return resp, nil +} + +func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( + resp *schema.GetOtherUserInfoByUsernameResp, err error) { + userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.NotFound(reason.UserNotFound) + } + resp = &schema.GetOtherUserInfoByUsernameResp{} + resp.ConvertFromUserEntityWithLang(ctx, userInfo) + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + + // Only the user himself and the administrator can see the hidden questions + questionCount, err := us.questionService.GetPersonalUserQuestionCount(ctx, req.UserID, userInfo.ID, req.IsAdmin) + if err != nil { + return nil, err + } + resp.QuestionCount = int(questionCount) + return resp, nil +} + +// EmailLogin email login +func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLoginReq) (resp *schema.UserLoginResp, err error) { + siteLogin, err := us.siteInfoService.GetSiteLogin(ctx) + if err != nil { + return nil, err + } + if !siteLogin.AllowPasswordLogin { + return nil, errors.BadRequest(reason.NotAllowedLoginViaPassword) + } + userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return nil, err + } + if !exist || userInfo.Status == entity.UserStatusDeleted { + return nil, errors.BadRequest(reason.EmailOrPasswordWrong) + } + if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) { + return nil, errors.BadRequest(reason.EmailOrPasswordWrong) + } + ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.BadRequest(reason.EmailOrPasswordWrong) + } + + err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID) + if err != nil { + log.Errorf("update last login data failed, err: %v", err) + } + + roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) + if err != nil { + log.Error(err) + } + + resp = &schema.UserLoginResp{} + resp.ConvertFromUserEntity(userInfo) + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + userCacheInfo := &entity.UserCacheInfo{ + UserID: userInfo.ID, + EmailStatus: userInfo.MailStatus, + UserStatus: userInfo.Status, + RoleID: roleID, + ExternalID: externalID, + } + resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) + if err != nil { + return nil, err + } + resp.RoleID = userCacheInfo.RoleID + if resp.RoleID == role.RoleAdminID { + err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, userCacheInfo) + if err != nil { + return nil, err + } + } + + return resp, nil +} + +// RetrievePassWord . +func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) error { + userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return err + } + if !has { + return nil + } + + // send email + data := &schema.EmailCodeContent{ + Email: req.Email, + UserID: userInfo.ID, + } + code := token.GenerateToken() + verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) + title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) + if err != nil { + return err + } + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) + return nil +} + +// UpdatePasswordWhenForgot update user password when user forgot password +func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) { + data := &schema.EmailCodeContent{} + err = data.FromJSONString(req.Content) + if err != nil { + return errors.BadRequest(reason.EmailVerifyURLExpired) + } + + userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + enpass, err := us.encryptPassword(ctx, req.Pass) + if err != nil { + return err + } + err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) + if err != nil { + return err + } + // When the user changes the password, all the current user's tokens are invalid. + us.authService.RemoveUserAllTokens(ctx, userInfo.ID) + return nil +} + +func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) { + userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID) + if err != nil { + return false, err + } + if !has { + return false, errors.BadRequest(reason.UserNotFound) + } + isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) + if !isPass { + return false, nil + } + + return true, nil +} + +// UserModifyPassword user modify password +func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error { + enpass, err := us.encryptPassword(ctx, req.Pass) + if err != nil { + return err + } + userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + + isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass) + if !isPass { + return errors.BadRequest(reason.OldPasswordVerificationFailed) + } + err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) + if err != nil { + return err + } + + us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken) + return nil +} + +// UpdateInfo update user info +func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) ( + errFields []*validator.FormErrorField, err error) { + + if len(req.Username) > 0 { + if checker.IsInvalidUsername(req.Username) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameInvalid, + }), errors.BadRequest(reason.UsernameInvalid) + } + // admin can use reserved username + if !req.IsAdmin && checker.IsReservedUsername(req.Username) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameInvalid, + }), errors.BadRequest(reason.UsernameInvalid) + } else if req.IsAdmin && checker.IsUsersIgnorePath(req.Username) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameInvalid, + }), errors.BadRequest(reason.UsernameInvalid) + } + + userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) + if err != nil { + return nil, err + } + if exist && userInfo.ID != req.UserID { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameDuplicate, + }), errors.BadRequest(reason.UsernameDuplicate) + } + } + + oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req) + + us.cleanUpRemovedAvatar(ctx, oldUserInfo.Avatar, cond.Avatar) + + err = us.userRepo.UpdateInfo(ctx, cond) + if err != nil { + return nil, err + } + us.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserUpdate, req.UserID)) + return nil, err +} + +func (us *UserService) cleanUpRemovedAvatar( + ctx context.Context, + oldAvatarJSON string, + newAvatarJSON string, +) { + if oldAvatarJSON == newAvatarJSON { + return + } + + var oldAvatar, newAvatar schema.AvatarInfo + + _ = json.Unmarshal([]byte(oldAvatarJSON), &oldAvatar) + _ = json.Unmarshal([]byte(newAvatarJSON), &newAvatar) + + if len(oldAvatar.Custom) == 0 { + return + } + + // clean up if old is custom and it's either removed or replaced + if oldAvatar.Custom != newAvatar.Custom { + fileRecord, err := us.fileRecordService.GetFileRecordByURL(ctx, oldAvatar.Custom) + if err != nil { + log.Error(err) + return + } + if fileRecord == nil { + log.Warn("no file record found for old avatar url:", oldAvatar.Custom) + return + } + if err := us.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { + log.Error(err) + } + } +} + +func (us *UserService) formatUserInfoForUpdateInfo( + oldUserInfo *entity.User, req *schema.UpdateInfoRequest) *entity.User { + avatar, _ := json.Marshal(req.Avatar) + + userInfo := &entity.User{} + userInfo.DisplayName = oldUserInfo.DisplayName + userInfo.Username = oldUserInfo.Username + userInfo.Avatar = oldUserInfo.Avatar + userInfo.Bio = oldUserInfo.Bio + userInfo.BioHTML = oldUserInfo.BioHTML + userInfo.Website = oldUserInfo.Website + userInfo.Location = oldUserInfo.Location + userInfo.ID = req.UserID + + if len(req.DisplayName) > 0 { + userInfo.DisplayName = req.DisplayName + } + if len(req.Username) > 0 { + userInfo.Username = req.Username + } + if len(avatar) > 0 { + userInfo.Avatar = string(avatar) + } + userInfo.Bio = req.Bio + userInfo.BioHTML = req.BioHTML + userInfo.Website = req.Website + userInfo.Location = req.Location + return userInfo +} + +// UserUpdateInterface update user interface +func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) { + return us.userRepo.UpdateUserInterface(ctx, req.UserId, req.Language, req.ColorScheme) +} + +// UserRegisterByEmail user register +func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( + resp *schema.UserLoginResp, errFields []*validator.FormErrorField, err error, +) { + _, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email) + if err != nil { + return nil, nil, err + } + if has { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "e_mail", + ErrorMsg: reason.EmailDuplicate, + }) + return nil, errFields, errors.BadRequest(reason.EmailDuplicate) + } + + userInfo := &entity.User{} + userInfo.EMail = registerUserInfo.Email + userInfo.DisplayName = registerUserInfo.Name + userInfo.Pass, err = us.encryptPassword(ctx, registerUserInfo.Pass) + if err != nil { + return nil, nil, err + } + userInfo.Username, err = us.userCommonService.MakeUsername(ctx, registerUserInfo.Name) + if err != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "name", + ErrorMsg: reason.UsernameInvalid, + }) + return nil, errFields, err + } + userInfo.IPInfo = registerUserInfo.IP + userInfo.MailStatus = entity.EmailStatusToBeVerified + userInfo.Status = entity.UserStatusAvailable + userInfo.LastLoginDate = time.Now() + err = us.userRepo.AddUser(ctx, userInfo) + if err != nil { + return nil, nil, err + } + if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{userInfo.ID}); err != nil { + log.Errorf("set default user notification config failed, err: %v", err) + } + + // send email + data := &schema.EmailCodeContent{ + Email: registerUserInfo.Email, + UserID: userInfo.ID, + } + code := token.GenerateToken() + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) + if err != nil { + return nil, nil, err + } + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) + + roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) + if err != nil { + log.Error(err) + } + + // return user info and token + resp = &schema.UserLoginResp{} + resp.ConvertFromUserEntity(userInfo) + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + userCacheInfo := &entity.UserCacheInfo{ + UserID: userInfo.ID, + EmailStatus: userInfo.MailStatus, + UserStatus: userInfo.Status, + RoleID: roleID, + } + resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) + if err != nil { + return nil, nil, err + } + resp.RoleID = userCacheInfo.RoleID + if resp.RoleID == role.RoleAdminID { + err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) + if err != nil { + return nil, nil, err + } + } + return resp, nil, nil +} + +func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) error { + userInfo, has, err := us.userRepo.GetByUserID(ctx, userID) + if err != nil { + return err + } + if !has { + return errors.BadRequest(reason.UserNotFound) + } + + data := &schema.EmailCodeContent{ + Email: userInfo.EMail, + UserID: userInfo.ID, + } + code := token.GenerateToken() + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) + if err != nil { + return err + } + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) + return nil +} + +func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.UserLoginResp, err error) { + data := &schema.EmailCodeContent{} + err = data.FromJSONString(req.Content) + if err != nil { + return nil, errors.BadRequest(reason.EmailVerifyURLExpired) + } + + userInfo, has, err := us.userRepo.GetByEmail(ctx, data.Email) + if err != nil { + return nil, err + } + if !has { + return nil, errors.BadRequest(reason.UserNotFound) + } + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + userInfo.MailStatus = entity.EmailStatusAvailable + err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus) + if err != nil { + return nil, err + } + } + if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { + log.Error(err) + return nil, err + } + + // In the case of three-party login, the associated users are bound + if len(data.BindingKey) > 0 { + err = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo) + if err != nil { + return nil, err + } + } + + accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo( + ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "") + if err != nil { + return nil, err + } + + resp = &schema.UserLoginResp{} + resp.ConvertFromUserEntity(userInfo) + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + resp.AccessToken = accessToken + // User verified email will update user email status. So user status cache should be updated. + if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil { + return nil, err + } + return resp, nil +} + +// verifyPassword +// Compare whether the password is correct +func (us *UserService) verifyPassword(ctx context.Context, loginPass, userPass string) bool { + if len(loginPass) == 0 && len(userPass) == 0 { + return true + } + err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass)) + return err == nil +} + +// encryptPassword +// The password does irreversible encryption. +func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string, error) { + hashPwd, err := bcrypt.GenerateFromPassword([]byte(Pass), bcrypt.DefaultCost) + // This encrypted string can be saved to the database and can be used as password matching verification + return string(hashPwd), err +} + +// UserChangeEmailSendCode user change email verification +func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) ( + resp []*validator.FormErrorField, err error) { + userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + // If user's email already verified, then must verify password first. + if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) { + resp = append(resp, &validator.FormErrorField{ + ErrorField: "pass", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed), + }) + return resp, errors.BadRequest(reason.OldPasswordVerificationFailed) + } + + _, exist, err = us.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return nil, err + } + if exist { + resp = append([]*validator.FormErrorField{}, &validator.FormErrorField{ + ErrorField: "e_mail", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailDuplicate), + }) + return resp, errors.BadRequest(reason.EmailDuplicate) + } + + data := &schema.EmailCodeContent{ + Email: req.Email, + UserID: req.UserID, + } + code := token.GenerateToken() + var title, body string + verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code) + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) + } else { + title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) + } + if err != nil { + return nil, err + } + log.Infof("send email confirmation %s", verifyEmailURL) + + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, req.Email, title, body, code, data.ToJSONString()) + return nil, nil +} + +// UserChangeEmailVerify user change email verify code +func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (resp *schema.UserLoginResp, err error) { + data := &schema.EmailCodeContent{} + err = data.FromJSONString(content) + if err != nil { + return nil, errors.BadRequest(reason.EmailVerifyURLExpired) + } + + _, exist, err := us.userRepo.GetByEmail(ctx, data.Email) + if err != nil { + return nil, err + } + if exist { + return nil, errors.BadRequest(reason.EmailDuplicate) + } + + userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email) + if err != nil { + return nil, errors.BadRequest(reason.UserNotFound) + } + err = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) + if err != nil { + return nil, err + } + // if email status is to be verified, active user as well + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { + log.Error(err) + return nil, err + } + } + + roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) + if err != nil { + log.Error(err) + } + + resp = &schema.UserLoginResp{} + resp.ConvertFromUserEntity(userInfo) + resp.Avatar = us.siteInfoService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() + userCacheInfo := &entity.UserCacheInfo{ + UserID: userInfo.ID, + EmailStatus: entity.EmailStatusAvailable, + UserStatus: userInfo.Status, + RoleID: roleID, + } + resp.AccessToken, resp.VisitToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) + if err != nil { + return nil, err + } + // User verified email will update user email status. So user status cache should be updated. + if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil { + return nil, err + } + resp.RoleID = userCacheInfo.RoleID + if resp.RoleID == role.RoleAdminID { + err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) + if err != nil { + return nil, err + } + } + return resp, nil +} + +// getSiteUrl get site url +func (us *UserService) getSiteUrl(ctx context.Context) string { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general failed: %s", err) + return "" + } + return siteGeneral.SiteUrl +} + +// UserRanking get user ranking +func (us *UserService) UserRanking(ctx context.Context) (resp *schema.UserRankingResp, err error) { + limit := 20 + endTime := time.Now() + startTime := endTime.AddDate(0, 0, -7) + userIDs, userIDExist := make([]string, 0), make(map[string]bool, 0) + + // get most reputation users + rankStat, rankStatUserIDs, err := us.getActivityUserRankStat(ctx, startTime, endTime, limit, userIDExist) + if err != nil { + return nil, err + } + userIDs = append(userIDs, rankStatUserIDs...) + + // get most vote users + voteStat, voteStatUserIDs, err := us.getActivityUserVoteStat(ctx, startTime, endTime, limit, userIDExist) + if err != nil { + return nil, err + } + userIDs = append(userIDs, voteStatUserIDs...) + + // get all staff members + userRoleRels, staffUserIDs, err := us.getStaff(ctx, userIDExist) + if err != nil { + return nil, err + } + userIDs = append(userIDs, staffUserIDs...) + + // get user information + userInfoMapping, err := us.getUserInfoMapping(ctx, userIDs) + if err != nil { + return nil, err + } + return us.warpStatRankingResp(userInfoMapping, rankStat, voteStat, userRoleRels), nil +} + +// GetUserStaff get user staff +func (us *UserService) GetUserStaff(ctx context.Context, req *schema.GetUserStaffReq) ( + resp []*schema.GetUserStaffResp, err error) { + userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, req.PageSize, true) + if err != nil { + return nil, err + } + avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList) + for _, u := range userList { + resp = append(resp, &schema.GetUserStaffResp{ + Username: u.Username, + DisplayName: u.DisplayName, + Avatar: avatarMapping[u.ID].GetURL(), + }) + } + return resp, nil +} + +// UserUnsubscribeNotification user unsubscribe email notification +func (us *UserService) UserUnsubscribeNotification( + ctx context.Context, req *schema.UserUnsubscribeNotificationReq) (err error) { + data := &schema.EmailCodeContent{} + err = data.FromJSONString(req.Content) + if err != nil || len(data.UserID) == 0 { + return errors.BadRequest(reason.EmailVerifyURLExpired) + } + + for _, source := range data.NotificationSources { + notificationConfig, exist, err := us.userNotificationConfigRepo.GetByUserIDAndSource( + ctx, data.UserID, source) + if err != nil { + return err + } + if !exist { + continue + } + channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) + // unsubscribe email notification + for _, channel := range channels { + if channel.Key == constant.EmailChannel { + channel.Enable = false + } + } + notificationConfig.Channels = channels.ToJsonString() + if err = us.userNotificationConfigRepo.Save(ctx, notificationConfig); err != nil { + return err + } + } + return nil +} + +func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int, + userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) { + if plugin.RankAgentEnabled() { + return make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil + } + rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit) + if err != nil { + return nil, nil, err + } + for _, stat := range rankStat { + if stat.Rank <= 0 { + continue + } + if userIDExist[stat.UserID] { + continue + } + userIDs = append(userIDs, stat.UserID) + userIDExist[stat.UserID] = true + } + return rankStat, userIDs, nil +} + +func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int, + userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) { + if plugin.RankAgentEnabled() { + return make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil + } + voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit) + if err != nil { + return nil, nil, err + } + for _, stat := range voteStat { + if stat.VoteCount <= 0 { + continue + } + if userIDExist[stat.UserID] { + continue + } + userIDs = append(userIDs, stat.UserID) + userIDExist[stat.UserID] = true + } + return voteStat, userIDs, nil +} + +func (us *UserService) getStaff(ctx context.Context, userIDExist map[string]bool) ( + userRoleRels []*entity.UserRoleRel, userIDs []string, err error) { + userRoleRels, err = us.userRoleService.GetUserByRoleID(ctx, []int{role.RoleAdminID, role.RoleModeratorID}) + if err != nil { + return nil, nil, err + } + for _, rel := range userRoleRels { + if userIDExist[rel.UserID] { + continue + } + userIDs = append(userIDs, rel.UserID) + userIDExist[rel.UserID] = true + } + return userRoleRels, userIDs, nil +} + +func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string) ( + userInfoMapping map[string]*entity.User, err error) { + userInfoMapping = make(map[string]*entity.User, 0) + if len(userIDs) == 0 { + return userInfoMapping, nil + } + userInfoList, err := us.userRepo.BatchGetByID(ctx, userIDs) + if err != nil { + return nil, err + } + avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userInfoList) + for _, user := range userInfoList { + user.Avatar = avatarMapping[user.ID].GetURL() + userInfoMapping[user.ID] = user + } + return userInfoMapping, nil +} + +func (us *UserService) SearchUserListByName(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) ( + resp []*schema.UserBasicInfo, err error) { + resp = make([]*schema.UserBasicInfo, 0) + if len(req.Username) == 0 { + return resp, nil + } + userList, err := us.userRepo.SearchUserListByName(ctx, req.Username, 5, false) + if err != nil { + return resp, err + } + avatarMapping := us.siteInfoService.FormatListAvatar(ctx, userList) + for _, u := range userList { + if req.UserID == u.ID { + continue + } + basicInfo := us.userCommonService.FormatUserBasicInfo(ctx, u) + basicInfo.Avatar = avatarMapping[u.ID].GetURL() + resp = append(resp, basicInfo) + } + return resp, nil +} + +func (us *UserService) warpStatRankingResp( + userInfoMapping map[string]*entity.User, + rankStat []*entity.ActivityUserRankStat, + voteStat []*entity.ActivityUserVoteStat, + userRoleRels []*entity.UserRoleRel) (resp *schema.UserRankingResp) { + resp = &schema.UserRankingResp{ + UsersWithTheMostReputation: make([]*schema.UserRankingSimpleInfo, 0), + UsersWithTheMostVote: make([]*schema.UserRankingSimpleInfo, 0), + Staffs: make([]*schema.UserRankingSimpleInfo, 0), + } + for _, stat := range rankStat { + if stat.Rank <= 0 { + continue + } + if userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { + resp.UsersWithTheMostReputation = append(resp.UsersWithTheMostReputation, &schema.UserRankingSimpleInfo{ + Username: userInfo.Username, + Rank: stat.Rank, + DisplayName: userInfo.DisplayName, + Avatar: userInfo.Avatar, + }) + } + } + for _, stat := range voteStat { + if stat.VoteCount <= 0 { + continue + } + if userInfo := userInfoMapping[stat.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { + resp.UsersWithTheMostVote = append(resp.UsersWithTheMostVote, &schema.UserRankingSimpleInfo{ + Username: userInfo.Username, + VoteCount: stat.VoteCount, + DisplayName: userInfo.DisplayName, + Avatar: userInfo.Avatar, + }) + } + } + for _, rel := range userRoleRels { + if userInfo := userInfoMapping[rel.UserID]; userInfo != nil && userInfo.Status != entity.UserStatusDeleted { + resp.Staffs = append(resp.Staffs, &schema.UserRankingSimpleInfo{ + Username: userInfo.Username, + Rank: userInfo.Rank, + DisplayName: userInfo.DisplayName, + Avatar: userInfo.Avatar, + }) + } + } + return resp +} diff --git a/internal/service/content/vote_service.go b/internal/service/content/vote_service.go new file mode 100644 index 000000000..ff3ee5974 --- /dev/null +++ b/internal/service/content/vote_service.go @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package content + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/service/event_queue" + "strings" + + "github.com/apache/answer/internal/service/activity_common" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/activity_type" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/pkg/htmltext" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/segmentfault/pacman/errors" +) + +// VoteRepo activity repository +type VoteRepo interface { + Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) + GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error) + ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) ( + voteList []*entity.Activity, total int64, err error) +} + +// VoteService user service +type VoteService struct { + voteRepo VoteRepo + configService *config.ConfigService + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentCommonRepo comment_common.CommentCommonRepo + objectService *object_info.ObjService + activityRepo activity_common.ActivityRepo + eventQueueService event_queue.EventQueueService +} + +func NewVoteService( + voteRepo VoteRepo, + configService *config.ConfigService, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + commentCommonRepo comment_common.CommentCommonRepo, + objectService *object_info.ObjService, + eventQueueService event_queue.EventQueueService, +) *VoteService { + return &VoteService{ + voteRepo: voteRepo, + configService: configService, + questionRepo: questionRepo, + answerRepo: answerRepo, + commentCommonRepo: commentCommonRepo, + objectService: objectService, + eventQueueService: eventQueueService, + } +} + +// VoteUp vote up +func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) + if err != nil { + return nil, err + } + if objectInfo.IsDeleted() { + return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) + } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID + + // check user is voting self or not + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) + } + + voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo) + + // vote operation + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo) + } else { + // cancel vote down if exist + voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + err = vs.voteRepo.CancelVote(ctx, voteOperationInfo) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteUpOperationInfo) + if err != nil { + return nil, err + } + } + if err != nil { + return nil, err + } + + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) + if err != nil { + log.Error(err) + } + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteUp + vs.sendEvent(ctx, req, objectInfo, resp) + } + return resp, nil +} + +// VoteDown vote down +func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) { + objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID) + if err != nil { + return nil, err + } + if objectInfo.IsDeleted() { + return nil, errors.BadRequest(reason.NewObjectAlreadyDeleted) + } + // make object id must be decoded + objectInfo.ObjectID = req.ObjectID + + // check user is voting self or not + if objectInfo.ObjectCreatorUserID == req.UserID { + return nil, errors.BadRequest(reason.DisallowVoteYourSelf) + } + + // vote operation + voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo) + if req.IsCancel { + err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo) + if err != nil { + return nil, err + } + } else { + // cancel vote up if exist + err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)) + if err != nil { + return nil, err + } + err = vs.voteRepo.Vote(ctx, voteDownOperationInfo) + if err != nil { + return nil, err + } + } + + resp = &schema.VoteResp{} + resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType) + if err != nil { + log.Error(err) + } + resp.Votes = resp.UpVotes - resp.DownVotes + if !req.IsCancel { + resp.VoteStatus = constant.ActVoteDown + vs.sendEvent(ctx, req, objectInfo, resp) + } + return resp, nil +} + +// ListUserVotes list user's votes +func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) { + typeKeys := []string{ + activity_type.QuestionVoteUp, + activity_type.QuestionVoteDown, + activity_type.AnswerVoteUp, + activity_type.AnswerVoteDown, + } + activityTypes := make([]int, 0) + activityTypeMapping := make(map[int]string, 0) + + for _, typeKey := range typeKeys { + cfg, err := vs.configService.GetConfigByKey(ctx, typeKey) + if err != nil { + continue + } + activityTypes = append(activityTypes, cfg.ID) + activityTypeMapping[cfg.ID] = typeKey + } + + voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes) + if err != nil { + return nil, err + } + + lang := handler.GetLangByCtx(ctx) + + votes := make([]*schema.GetVoteWithPageResp, 0) + for _, voteInfo := range voteList { + objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) + if err != nil { + log.Error(err) + continue + } + + item := &schema.GetVoteWithPageResp{ + CreatedAt: voteInfo.CreatedAt.Unix(), + ObjectID: objInfo.ObjectID, + QuestionID: objInfo.QuestionID, + AnswerID: objInfo.AnswerID, + ObjectType: objInfo.ObjectType, + Title: objInfo.Title, + UrlTitle: htmltext.UrlTitle(objInfo.Title), + Content: objInfo.Content, + } + item.VoteType = translator.Tr(lang, + activity_type.ActivityTypeFlagMapping[activityTypeMapping[voteInfo.ActivityType]]) + if objInfo.QuestionStatus == entity.QuestionStatusDeleted { + item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) + } + votes = append(votes, item) + } + return pager.NewPageModel(total, votes), err +} + +func (vs *VoteService) createVoteOperationInfo(ctx context.Context, + userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo { + // warp vote operation + voteOperationInfo := &schema.VoteOperationInfo{ + ObjectID: objectInfo.ObjectID, + ObjectType: objectInfo.ObjectType, + ObjectCreatorUserID: objectInfo.ObjectCreatorUserID, + OperatingUserID: userID, + VoteUp: voteUp, + VoteDown: !voteUp, + } + voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo) + return voteOperationInfo +} + +func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) ( + activities []*schema.VoteActivity) { + activities = make([]*schema.VoteActivity, 0) + + var actions []string + switch op.ObjectType { + case constant.QuestionObjectType: + if op.VoteUp { + actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp} + } else { + actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown} + } + case constant.AnswerObjectType: + if op.VoteUp { + actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp} + } else { + actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown} + } + case constant.CommentObjectType: + actions = []string{activity_type.CommentVoteUp} + } + + for _, action := range actions { + t := &schema.VoteActivity{} + cfg, err := vs.configService.GetConfigByKey(ctx, action) + if err != nil { + log.Warnf("get config by key error: %v", err) + continue + } + t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue() + + if strings.Contains(action, "voted") { + t.ActivityUserID = op.ObjectCreatorUserID + t.TriggerUserID = op.OperatingUserID + } else { + t.ActivityUserID = op.OperatingUserID + t.TriggerUserID = "0" + } + activities = append(activities, t) + } + return activities +} + +func (vs *VoteService) sendEvent(ctx context.Context, + req *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) { + var event *schema.EventMsg + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + event = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID). + QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) + case constant.AnswerObjectType: + event = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID). + AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) + case constant.CommentObjectType: + event = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID). + CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) + default: + return + } + event.AddExtra("vote_up_amount", fmt.Sprintf("%d", resp.UpVotes)) + event.AddExtra("vote_down_amount", fmt.Sprintf("%d", resp.DownVotes)) + vs.eventQueueService.Send(ctx, event) +} diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go new file mode 100644 index 000000000..d9198e6db --- /dev/null +++ b/internal/service/dashboard/dashboard_service.go @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package dashboard + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision" + "github.com/apache/answer/pkg/converter" + "xorm.io/xorm/schemas" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/export" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/report_common" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/dir" + "github.com/segmentfault/pacman/log" +) + +type dashboardService struct { + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentRepo comment_common.CommentCommonRepo + voteRepo activity_common.VoteRepo + userRepo usercommon.UserRepo + reportRepo report_common.ReportRepo + configService *config.ConfigService + siteInfoService siteinfo_common.SiteInfoCommonService + serviceConfig *service_config.ServiceConfig + reviewService *review.ReviewService + revisionRepo revision.RevisionRepo + data *data.Data +} + +func NewDashboardService( + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + commentRepo comment_common.CommentCommonRepo, + voteRepo activity_common.VoteRepo, + userRepo usercommon.UserRepo, + reportRepo report_common.ReportRepo, + configService *config.ConfigService, + siteInfoService siteinfo_common.SiteInfoCommonService, + serviceConfig *service_config.ServiceConfig, + reviewService *review.ReviewService, + revisionRepo revision.RevisionRepo, + data *data.Data, +) DashboardService { + return &dashboardService{ + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + voteRepo: voteRepo, + userRepo: userRepo, + reportRepo: reportRepo, + configService: configService, + siteInfoService: siteInfoService, + serviceConfig: serviceConfig, + reviewService: reviewService, + revisionRepo: revisionRepo, + data: data, + } +} + +type DashboardService interface { + Statistical(ctx context.Context) (resp *schema.DashboardInfo, err error) +} + +func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { + dashboardInfo := ds.getFromCache(ctx) + if dashboardInfo == nil { + dashboardInfo = &schema.DashboardInfo{} + dashboardInfo.AnswerCount = ds.answerCount(ctx) + dashboardInfo.CommentCount = ds.commentCount(ctx) + dashboardInfo.UserCount = ds.userCount(ctx) + dashboardInfo.VoteCount = ds.voteCount(ctx) + dashboardInfo.OccupyingStorageSpace = ds.calculateStorage() + general, err := ds.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get general site info failed: %s", err) + return dashboardInfo, nil + } + if general.CheckUpdate { + dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) + } + dashboardInfo.DatabaseVersion = ds.getDatabaseInfo() + dashboardInfo.DatabaseSize = ds.GetDatabaseSize() + } + + dashboardInfo.QuestionCount = ds.questionCount(ctx) + dashboardInfo.UnansweredCount = ds.unansweredQuestionCount(ctx) + dashboardInfo.ResolvedCount = ds.resolvedQuestionCount(ctx) + + if dashboardInfo.QuestionCount == 0 { + dashboardInfo.ResolvedRate = "0.00" + dashboardInfo.UnansweredRate = "0.00" + } else { + dashboardInfo.ResolvedRate = fmt.Sprintf("%.2f", float64(dashboardInfo.ResolvedCount)/float64(dashboardInfo.QuestionCount)*100) + dashboardInfo.UnansweredRate = fmt.Sprintf("%.2f", float64(dashboardInfo.UnansweredCount)/float64(dashboardInfo.QuestionCount)*100) + } + + dashboardInfo.ReportCount = ds.reportCount(ctx) + dashboardInfo.SMTP = ds.smtpStatus(ctx) + dashboardInfo.HTTPS = ds.httpsStatus(ctx) + dashboardInfo.TimeZone = ds.getTimezone(ctx) + dashboardInfo.UploadingFiles = true + dashboardInfo.AppStartTime = fmt.Sprintf("%d", time.Now().Unix()-schema.AppStartTime.Unix()) + dashboardInfo.VersionInfo.Version = constant.Version + dashboardInfo.VersionInfo.Revision = constant.Revision + dashboardInfo.GoVersion = constant.GoVersion + if siteLogin, err := ds.siteInfoService.GetSiteLogin(ctx); err == nil { + dashboardInfo.LoginRequired = siteLogin.LoginRequired + } + + ds.setCache(ctx, dashboardInfo) + return dashboardInfo, nil +} + +func (ds *dashboardService) getFromCache(ctx context.Context) (dashboardInfo *schema.DashboardInfo) { + infoStr, exist, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey) + if err != nil { + log.Errorf("get dashboard statistical from cache failed: %s", err) + return nil + } + if !exist { + return nil + } + dashboardInfo = &schema.DashboardInfo{} + if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil { + return nil + } + return dashboardInfo +} + +func (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) { + infoStr, _ := json.Marshal(info) + err := ds.data.Cache.SetString(ctx, schema.DashboardCacheKey, string(infoStr), schema.DashboardCacheTime) + if err != nil { + log.Errorf("set dashboard statistical failed: %s", err) + } +} + +func (ds *dashboardService) questionCount(ctx context.Context) int64 { + questionCount, err := ds.questionRepo.GetQuestionCount(ctx) + if err != nil { + log.Errorf("get question count failed: %s", err) + } + return questionCount +} + +func (ds *dashboardService) unansweredQuestionCount(ctx context.Context) int64 { + unansweredQuestionCount, err := ds.questionRepo.GetUnansweredQuestionCount(ctx) + if err != nil { + log.Errorf("get unanswered question count failed: %s", err) + } + return unansweredQuestionCount +} + +func (ds *dashboardService) resolvedQuestionCount(ctx context.Context) int64 { + resolvedQuestionCount, err := ds.questionRepo.GetResolvedQuestionCount(ctx) + if err != nil { + log.Errorf("get resolved question count failed: %s", err) + } + return resolvedQuestionCount +} + +func (ds *dashboardService) answerCount(ctx context.Context) int64 { + answerCount, err := ds.answerRepo.GetAnswerCount(ctx) + if err != nil { + log.Errorf("get answer count failed: %s", err) + } + return answerCount +} + +func (ds *dashboardService) commentCount(ctx context.Context) int64 { + commentCount, err := ds.commentRepo.GetCommentCount(ctx) + if err != nil { + log.Errorf("get comment count failed: %s", err) + } + return commentCount +} + +func (ds *dashboardService) userCount(ctx context.Context) int64 { + userCount, err := ds.userRepo.GetUserCount(ctx) + if err != nil { + log.Errorf("get user count failed: %s", err) + } + return userCount +} + +func (ds *dashboardService) reportCount(ctx context.Context) int64 { + reviewCount, err := ds.reviewService.GetReviewPendingCount(ctx) + if err != nil { + log.Errorf("get review count failed: %s", err) + } + reportCount, err := ds.reportRepo.GetReportCount(ctx) + if err != nil { + log.Errorf("get report count failed: %s", err) + } + countUnreviewedRevision, err := ds.revisionRepo.CountUnreviewedRevision(ctx, []int{ + constant.ObjectTypeStrMapping[constant.AnswerObjectType], + constant.ObjectTypeStrMapping[constant.QuestionObjectType], + constant.ObjectTypeStrMapping[constant.TagObjectType], + }) + if err != nil { + log.Errorf("get revision count failed: %s", err) + } + return reviewCount + reportCount + countUnreviewedRevision +} + +// count vote +func (ds *dashboardService) voteCount(ctx context.Context) int64 { + typeKeys := []string{ + "question.vote_up", + "question.vote_down", + "answer.vote_up", + "answer.vote_down", + } + var activityTypes []int + for _, typeKey := range typeKeys { + cfg, err := ds.configService.GetConfigByKey(ctx, typeKey) + if err != nil { + continue + } + activityTypes = append(activityTypes, cfg.ID) + } + voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) + if err != nil { + log.Errorf("get vote count failed: %s", err) + } + return voteCount +} + +func (ds *dashboardService) remoteVersion(ctx context.Context) string { + req, _ := http.NewRequest("GET", "https://answer.apache.org/data/latest.json?from_version="+constant.Version, nil) + req.Header.Set("User-Agent", "Answer/"+constant.Version) + httpClient := &http.Client{} + httpClient.Timeout = 15 * time.Second + resp, err := httpClient.Do(req) + if err != nil { + log.Errorf("request remote version failed: %s", err) + return "" + } + defer resp.Body.Close() + + respByte, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("read response body failed: %s", err) + return "" + } + remoteVersion := &schema.RemoteVersion{} + if err := json.Unmarshal(respByte, remoteVersion); err != nil { + log.Errorf("parsing response body failed: %s", err) + return "" + } + return remoteVersion.Release.Version +} + +func (ds *dashboardService) smtpStatus(ctx context.Context) (smtpStatus string) { + smtpStatus = "not_configured" + emailConf, err := ds.configService.GetStringValue(ctx, "email.config") + if err != nil { + log.Errorf("get email config failed: %s", err) + return "disabled" + } + ec := &export.EmailConfig{} + err = json.Unmarshal([]byte(emailConf), ec) + if err != nil { + log.Errorf("parsing email config failed: %s", err) + return "disabled" + } + if ec.SMTPHost != "" { + smtpStatus = "enabled" + } + return smtpStatus +} + +func (ds *dashboardService) httpsStatus(ctx context.Context) (enabled bool) { + siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general failed: %s", err) + return false + } + siteUrl, err := url.Parse(siteGeneral.SiteUrl) + if err != nil { + log.Errorf("parse site url failed: %s", err) + return false + } + return siteUrl.Scheme == "https" +} + +func (ds *dashboardService) getTimezone(ctx context.Context) string { + siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return "" + } + return siteInfoInterface.TimeZone +} + +func (ds *dashboardService) calculateStorage() string { + dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath) + if err != nil { + log.Errorf("get upload dir size failed: %s", err) + return "" + } + return dir.FormatFileSize(dirSize) +} + +func (ds *dashboardService) getDatabaseInfo() (versionDesc string) { + dbVersion, err := ds.data.DB.DBVersion() + if err != nil { + log.Errorf("get db version failed: %s", err) + } else { + versionDesc = fmt.Sprintf("%s %s", ds.data.DB.Dialect().URI().DBType, dbVersion.Number) + } + return versionDesc +} + +func (ds *dashboardService) GetDatabaseSize() (dbSize string) { + switch ds.data.DB.Dialect().URI().DBType { + case schemas.MYSQL: + sql := fmt.Sprintf("SELECT SUM(DATA_LENGTH) as db_size FROM information_schema.TABLES WHERE table_schema = '%s'", + ds.data.DB.Dialect().URI().DBName) + res, err := ds.data.DB.QueryInterface(sql) + if err != nil { + log.Warnf("get db size failed: %s", err) + } else { + if res != nil && len(res) > 0 && res[0]["db_size"] != nil { + dbSizeStr, _ := res[0]["db_size"].(string) + dbSize = dir.FormatFileSize(converter.StringToInt64(dbSizeStr)) + } + } + case schemas.POSTGRES: + sql := fmt.Sprintf("SELECT pg_database_size('%s') AS db_size", + ds.data.DB.Dialect().URI().DBName) + res, err := ds.data.DB.QueryInterface(sql) + if err != nil { + log.Warnf("get db size failed: %s", err) + } else { + if res != nil && len(res) > 0 && res[0]["db_size"] != nil { + dbSizeStr, _ := res[0]["db_size"].(int32) + dbSize = dir.FormatFileSize(int64(dbSizeStr)) + } + } + case schemas.SQLITE: + dirSize, err := dir.DirSize(ds.data.DB.DataSourceName()) + if err != nil { + log.Errorf("get upload dir size failed: %s", err) + return "" + } + dbSize = dir.FormatFileSize(dirSize) + } + return dbSize +} diff --git a/internal/service/dashboard/dashboard_test.go b/internal/service/dashboard/dashboard_test.go new file mode 100644 index 000000000..b858597a2 --- /dev/null +++ b/internal/service/dashboard/dashboard_test.go @@ -0,0 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package dashboard diff --git a/internal/service/event_queue/event_queue.go b/internal/service/event_queue/event_queue.go new file mode 100644 index 000000000..77dc302b5 --- /dev/null +++ b/internal/service/event_queue/event_queue.go @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package event_queue + +import ( + "context" + + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" +) + +type EventQueueService interface { + Send(ctx context.Context, msg *schema.EventMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.EventMsg) error) +} + +type eventQueueService struct { + Queue chan *schema.EventMsg + Handler func(ctx context.Context, msg *schema.EventMsg) error +} + +func (ns *eventQueueService) Send(ctx context.Context, msg *schema.EventMsg) { + ns.Queue <- msg +} + +func (ns *eventQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.EventMsg) error) { + ns.Handler = handler +} + +func (ns *eventQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received badge %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for badge") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewEventQueueService create a new badge queue service +func NewEventQueueService() EventQueueService { + ns := &eventQueueService{} + ns.Queue = make(chan *schema.EventMsg, 128) + ns.working() + return ns +} diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 420585430..b00ee093b 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -1,17 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package export import ( - "bytes" + "crypto/tls" "encoding/json" "fmt" - "html/template" + "github.com/apache/answer/pkg/display" "mime" - - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/siteinfo_common" + "os" + "strings" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/siteinfo_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/net/context" @@ -20,23 +44,27 @@ import ( // EmailService kit service type EmailService struct { - configRepo config.ConfigRepo - emailRepo EmailRepo - siteInfoRepo siteinfo_common.SiteInfoRepo + configService *config.ConfigService + emailRepo EmailRepo + siteInfoService siteinfo_common.SiteInfoCommonService } // EmailRepo email repository type EmailRepo interface { - SetCode(ctx context.Context, code, content string) error + SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error VerifyCode(ctx context.Context, code string) (content string, err error) } // NewEmailService email service -func NewEmailService(configRepo config.ConfigRepo, emailRepo EmailRepo, siteInfoRepo siteinfo_common.SiteInfoRepo) *EmailService { +func NewEmailService( + configService *config.ConfigService, + emailRepo EmailRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *EmailService { return &EmailService{ - configRepo: configRepo, - emailRepo: emailRepo, - siteInfoRepo: siteInfoRepo, + configService: configService, + emailRepo: emailRepo, + siteInfoService: siteInfoService, } } @@ -46,52 +74,61 @@ type EmailConfig struct { FromName string `json:"from_name"` SMTPHost string `json:"smtp_host"` SMTPPort int `json:"smtp_port"` - Encryption string `json:"encryption"` // "" SSL + Encryption string `json:"encryption"` // "" SSL TLS SMTPUsername string `json:"smtp_username"` SMTPPassword string `json:"smtp_password"` SMTPAuthentication bool `json:"smtp_authentication"` - - RegisterTitle string `json:"register_title"` - RegisterBody string `json:"register_body"` - PassResetTitle string `json:"pass_reset_title"` - PassResetBody string `json:"pass_reset_body"` - ChangeTitle string `json:"change_title"` - ChangeBody string `json:"change_body"` - TestTitle string `json:"test_title"` - TestBody string `json:"test_body"` } func (e *EmailConfig) IsSSL() bool { return e.Encryption == "SSL" } -type RegisterTemplateData struct { - SiteName string - RegisterUrl string +func (e *EmailConfig) IsTLS() bool { + return e.Encryption == "TLS" } -type PassResetTemplateData struct { - SiteName string - PassResetUrl string +// SaveCode save code +func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) + if err != nil { + log.Error(err) + } } -type ChangeEmailTemplateData struct { - SiteName string - ChangeEmailUrl string +// SendAndSaveCode send email and save code +func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime) + if err != nil { + log.Error(err) + return + } + es.Send(ctx, toEmailAddr, subject, body) } -type TestTemplateData struct { - SiteName string +// SendAndSaveCodeWithTime send email and save code +func (es *EmailService) SendAndSaveCodeWithTime( + ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) { + err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration) + if err != nil { + log.Error(err) + return + } + es.Send(ctx, toEmailAddr, subject, body) } // Send email send -func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, code, codeContent string) { +func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) { log.Infof("try to send email to %s", toEmailAddr) - ec, err := es.GetEmailConfig() + ec, err := es.GetEmailConfig(ctx) if err != nil { log.Errorf("get email config failed: %s", err) return } + if len(ec.SMTPHost) == 0 { + log.Warnf("smtp host is empty, skip send email") + return + } m := gomail.NewMessage() fromName := mime.QEncoding.Encode("utf-8", ec.FromName) @@ -104,18 +141,17 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, co if ec.IsSSL() { d.SSL = true } + if ec.IsTLS() { + d.SSL = false + } + if len(os.Getenv("SKIP_SMTP_TLS_VERIFY")) > 0 { + d.TLSConfig = &tls.Config{ServerName: d.Host, InsecureSkipVerify: true} + } if err := d.DialAndSend(m); err != nil { log.Errorf("send email to %s failed: %s", toEmailAddr, err) } else { log.Infof("send email to %s success", toEmailAddr) } - - if len(code) > 0 { - err = es.emailRepo.SetCode(ctx, code, codeContent) - if err != nil { - log.Error(err) - } - } } // VerifyUrlExpired email send @@ -127,178 +163,186 @@ func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (cont return content } -func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = es.siteInfoRepo.GetByType(ctx, siteType) - if !exist { +func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) + if err != nil { return } + templateData := &schema.RegisterTemplateData{ + SiteName: siteInfo.Name, + RegisterUrl: registerUrl, + } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyRegisterTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyRegisterBody, templateData) + return title, body, nil } -func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) { - ec, err := es.GetEmailConfig() - if err != nil { - return - } - siteinfo, err := es.GetSiteGeneral(ctx) +func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } - templateData := RegisterTemplateData{ - SiteName: siteinfo.Name, RegisterUrl: registerUrl, - } - tmpl, err := template.New("register_title").Parse(ec.RegisterTitle) - if err != nil { - return "", "", err - } - titleBuf := &bytes.Buffer{} - bodyBuf := &bytes.Buffer{} - err = tmpl.Execute(titleBuf, templateData) - if err != nil { - return "", "", err - } + templateData := &schema.PassResetTemplateData{SiteName: siteInfo.Name, PassResetUrl: passResetUrl} + + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyPassResetTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyPassResetBody, templateData) + return title, body, nil +} - tmpl, err = template.New("register_body").Parse(ec.RegisterBody) +func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { - return "", "", err + return } - err = tmpl.Execute(bodyBuf, templateData) - if err != nil { - return "", "", err + templateData := &schema.ChangeEmailTemplateData{ + SiteName: siteInfo.Name, + ChangeEmailUrl: changeEmailUrl, } - return titleBuf.String(), bodyBuf.String(), nil + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyChangeEmailBody, templateData) + return title, body, nil } -func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) { - ec, err := es.GetEmailConfig() +// TestTemplate send test email template parse +func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } + templateData := &schema.TestTemplateData{SiteName: siteInfo.Name} + + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyTestTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyTestBody, templateData) + return title, body, nil +} - siteinfo, err := es.GetSiteGeneral(ctx) +// NewAnswerTemplate new answer template +func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } - - templateData := PassResetTemplateData{SiteName: siteinfo.Name, PassResetUrl: passResetUrl} - tmpl, err := template.New("pass_reset_title").Parse(ec.PassResetTitle) + seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { - return "", "", err + return } - titleBuf := &bytes.Buffer{} - bodyBuf := &bytes.Buffer{} - err = tmpl.Execute(titleBuf, templateData) - if err != nil { - return "", "", err + templateData := &schema.NewAnswerTemplateData{ + SiteName: siteInfo.Name, + DisplayName: raw.AnswerUserDisplayName, + QuestionTitle: raw.QuestionTitle, + AnswerUrl: display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID), + AnswerSummary: raw.AnswerSummary, + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } - tmpl, err = template.New("pass_reset_body").Parse(ec.PassResetBody) - if err != nil { - return "", "", err - } - err = tmpl.Execute(bodyBuf, templateData) - if err != nil { - return "", "", err - } - return titleBuf.String(), bodyBuf.String(), nil + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData) + return title, body, nil } -func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl string) (title, body string, err error) { - ec, err := es.GetEmailConfig() +// NewInviteAnswerTemplate new invite answer template +func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } - - siteinfo, err := es.GetSiteGeneral(ctx) + seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } - templateData := ChangeEmailTemplateData{ - SiteName: siteinfo.Name, - ChangeEmailUrl: changeEmailUrl, - } - tmpl, err := template.New("email_change_title").Parse(ec.ChangeTitle) - if err != nil { - return "", "", err - } - titleBuf := &bytes.Buffer{} - bodyBuf := &bytes.Buffer{} - err = tmpl.Execute(titleBuf, templateData) - if err != nil { - return "", "", err + templateData := &schema.NewInviteAnswerTemplateData{ + SiteName: siteInfo.Name, + DisplayName: raw.InviterDisplayName, + QuestionTitle: raw.QuestionTitle, + InviteUrl: display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle), + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } - tmpl, err = template.New("email_change_body").Parse(ec.ChangeBody) - if err != nil { - return "", "", err - } - err = tmpl.Execute(bodyBuf, templateData) - if err != nil { - return "", "", err - } - return titleBuf.String(), bodyBuf.String(), nil + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData) + return title, body, nil } -func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) { - ec, err := es.GetEmailConfig() +// NewCommentTemplate new comment template +func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { return } - - siteinfo, err := es.GetSiteGeneral(ctx) + seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { return } - - templateData := TestTemplateData{ - SiteName: siteinfo.Name, + templateData := &schema.NewCommentTemplateData{ + SiteName: siteInfo.Name, + DisplayName: raw.CommentUserDisplayName, + QuestionTitle: raw.QuestionTitle, + CommentSummary: raw.CommentSummary, + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } + templateData.CommentUrl = display.CommentURL(seoInfo.Permalink, + siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle, raw.AnswerID, raw.CommentID) - titleBuf := &bytes.Buffer{} - bodyBuf := &bytes.Buffer{} + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData) + return title, body, nil +} - tmpl, err := template.New("test_title").Parse(ec.TestTitle) +// NewQuestionTemplate new question template +func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) ( + title, body string, err error) { + siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx) if err != nil { - return "", "", fmt.Errorf("email test title template parse error: %s", err) + return } - err = tmpl.Execute(titleBuf, templateData) + seoInfo, err := es.siteInfoService.GetSiteSeo(ctx) if err != nil { - return "", "", fmt.Errorf("email test body template parse error: %s", err) + return } - tmpl, err = template.New("test_body").Parse(ec.TestBody) - err = tmpl.Execute(bodyBuf, templateData) - if err != nil { - return "", "", err + templateData := &schema.NewQuestionTemplateData{ + SiteName: siteInfo.Name, + QuestionTitle: raw.QuestionTitle, + Tags: strings.Join(raw.Tags, ", "), + UnsubscribeUrl: fmt.Sprintf("%s/users/unsubscribe?code=%s", siteInfo.SiteUrl, raw.UnsubscribeCode), } - return titleBuf.String(), bodyBuf.String(), nil + templateData.QuestionUrl = display.QuestionURL( + seoInfo.Permalink, siteInfo.SiteUrl, raw.QuestionID, raw.QuestionTitle) + + lang := handler.GetLangByCtx(ctx) + title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData) + body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData) + return title, body, nil } -func (es *EmailService) GetEmailConfig() (ec *EmailConfig, err error) { - emailConf, err := es.configRepo.GetString("email.config") +func (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, err error) { + emailConf, err := es.configService.GetStringValue(ctx, constant.EmailConfigKey) if err != nil { return nil, err } ec = &EmailConfig{} err = json.Unmarshal([]byte(emailConf), ec) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + log.Errorf("old email config format is invalid, you need to update smtp config: %v", err) + return nil, errors.BadRequest(reason.SiteInfoConfigNotFound) } return ec, nil } // SetEmailConfig set email config -func (es *EmailService) SetEmailConfig(ec *EmailConfig) (err error) { +func (es *EmailService) SetEmailConfig(ctx context.Context, ec *EmailConfig) (err error) { data, _ := json.Marshal(ec) - return es.configRepo.SetConfig("email.config", string(data)) + return es.configService.UpdateConfig(ctx, constant.EmailConfigKey, string(data)) } diff --git a/internal/service/file_record/file_record_service.go b/internal/service/file_record/file_record_service.go new file mode 100644 index 000000000..29097ba8c --- /dev/null +++ b/internal/service/file_record/file_record_service.go @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package file_record + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/revision" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/pkg/writer" + "github.com/segmentfault/pacman/log" +) + +// FileRecordRepo file record repository +type FileRecordRepo interface { + AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) + UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) + GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) ( + fileRecordList []*entity.FileRecord, total int64, err error) + DeleteFileRecord(ctx context.Context, id int) (err error) + GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) +} + +// FileRecordService file record service +type FileRecordService struct { + fileRecordRepo FileRecordRepo + revisionRepo revision.RevisionRepo + serviceConfig *service_config.ServiceConfig + siteInfoService siteinfo_common.SiteInfoCommonService + userService *usercommon.UserCommon +} + +// NewFileRecordService new file record service +func NewFileRecordService( + fileRecordRepo FileRecordRepo, + revisionRepo revision.RevisionRepo, + serviceConfig *service_config.ServiceConfig, + siteInfoService siteinfo_common.SiteInfoCommonService, + userService *usercommon.UserCommon, +) *FileRecordService { + return &FileRecordService{ + fileRecordRepo: fileRecordRepo, + revisionRepo: revisionRepo, + serviceConfig: serviceConfig, + siteInfoService: siteInfoService, + userService: userService, + } +} + +// AddFileRecord add file record +func (fs *FileRecordService) AddFileRecord(ctx context.Context, userID, filePath, fileURL, source string) { + record := &entity.FileRecord{ + UserID: userID, + FilePath: filePath, + FileURL: fileURL, + Source: source, + Status: entity.FileRecordStatusAvailable, + ObjectID: "0", + } + if err := fs.fileRecordRepo.AddFileRecord(ctx, record); err != nil { + log.Errorf("add file record error: %v", err) + } +} + +// CleanOrphanUploadFiles clean orphan upload files +func (fs *FileRecordService) CleanOrphanUploadFiles(ctx context.Context) { + page, pageSize := 1, 1000 + + for { + fileRecordList, total, err := fs.fileRecordRepo.GetFileRecordPage(ctx, page, pageSize, &entity.FileRecord{ + Status: entity.FileRecordStatusAvailable, + }) + if err != nil { + log.Errorf("get file record page error: %v", err) + return + } + if len(fileRecordList) == 0 || total == 0 { + break + } + for _, fileRecord := range fileRecordList { + // If this file record created in 48 hours, no need to check + if fileRecord.CreatedAt.AddDate(0, 0, 2).After(time.Now()) { + continue + } + if isBrandingOrAvatarFile(fileRecord.FilePath) { + if strings.Contains(fileRecord.FilePath, constant.BrandingSubPath+"/") { + if fs.siteInfoService.IsBrandingFileUsed(ctx, fileRecord.FilePath) { + continue + } + } else if strings.Contains(fileRecord.FilePath, constant.AvatarSubPath+"/") { + if fs.userService.IsAvatarFileUsed(ctx, fileRecord.FilePath) { + continue + } + } + if err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { + log.Error(err) + } + continue + } + if checker.IsNotZeroString(fileRecord.ObjectID) { + _, exist, err := fs.revisionRepo.GetLastRevisionByObjectID(ctx, fileRecord.ObjectID) + if err != nil { + log.Errorf("get last revision by object id error: %v", err) + continue + } + if exist { + continue + } + } else { + lastRevision, exist, err := fs.revisionRepo.GetLastRevisionByFileURL(ctx, fileRecord.FileURL) + if err != nil { + log.Errorf("get last revision by file url error: %v", err) + continue + } + if exist { + // update the file record object id + fileRecord.ObjectID = lastRevision.ObjectID + if err := fs.fileRecordRepo.UpdateFileRecord(ctx, fileRecord); err != nil { + log.Errorf("update file record object id error: %v", err) + } + continue + } + } + // Delete and move the file record + if err := fs.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { + log.Error(err) + } + } + page++ + } +} + +func isBrandingOrAvatarFile(filePath string) bool { + return strings.Contains(filePath, constant.BrandingSubPath+"/") || strings.Contains(filePath, constant.AvatarSubPath+"/") +} + +func (fs *FileRecordService) PurgeDeletedFiles(ctx context.Context) { + deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath) + log.Infof("purge deleted files: %s", deletedPath) + err := os.RemoveAll(deletedPath) + if err != nil { + log.Errorf("purge deleted files error: %v", err) + return + } + err = dir.CreateDirIfNotExist(deletedPath) + if err != nil { + log.Errorf("create deleted directory error: %v", err) + } + return +} + +func (fs *FileRecordService) DeleteAndMoveFileRecord(ctx context.Context, fileRecord *entity.FileRecord) error { + // Delete the file record + if err := fs.fileRecordRepo.DeleteFileRecord(ctx, fileRecord.ID); err != nil { + return fmt.Errorf("delete file record error: %v", err) + } + + // Move the file to the deleted directory + oldFilename := filepath.Base(fileRecord.FilePath) + oldFilePath := filepath.Join(fs.serviceConfig.UploadPath, fileRecord.FilePath) + deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath, oldFilename) + + if err := writer.MoveFile(oldFilePath, deletedPath); err != nil { + return fmt.Errorf("move file error: %v", err) + } + + log.Debugf("delete and move file: %s", fileRecord.FileURL) + return nil +} + +func (fs *FileRecordService) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) { + record, err = fs.fileRecordRepo.GetFileRecordByURL(ctx, fileURL) + if err != nil { + log.Errorf("error retrieving file record by URL: %v", err) + return + } + return +} diff --git a/internal/service/follow/follow_service.go b/internal/service/follow/follow_service.go index e383d119e..743b0da67 100644 --- a/internal/service/follow/follow_service.go +++ b/internal/service/follow/follow_service.go @@ -1,12 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package follow import ( "context" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" ) type FollowRepo interface { @@ -15,7 +34,7 @@ type FollowRepo interface { } type FollowService struct { - tagRepo tagcommon.TagRepo + tagRepo tagcommon.TagCommonRepo followRepo FollowRepo followCommonRepo activity_common.FollowRepo } @@ -23,7 +42,7 @@ type FollowService struct { func NewFollowService( followRepo FollowRepo, followCommonRepo activity_common.FollowRepo, - tagRepo tagcommon.TagRepo, + tagRepo tagcommon.TagCommonRepo, ) *FollowService { return &FollowService{ followRepo: followRepo, diff --git a/internal/service/importer/importer_service.go b/internal/service/importer/importer_service.go new file mode 100644 index 000000000..45aabf39b --- /dev/null +++ b/internal/service/importer/importer_service.go @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package importer + +import ( + "context" + "fmt" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/rank" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/plugin" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// ImporterService importer service +type ImporterService struct { + questionService *content.QuestionService + rankService *rank.RankService + userCommon *usercommon.UserCommon +} + +// NewRankService new rank service +func NewImporterService( + questionService *content.QuestionService, + rankService *rank.RankService, + userCommon *usercommon.UserCommon) *ImporterService { + return &ImporterService{ + questionService: questionService, + rankService: rankService, + userCommon: userCommon, + } +} + +type ImporterFunc struct { + importerService *ImporterService +} + +func (ipfunc *ImporterFunc) AddQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) { + ipfunc.importerService.ImportQuestion(ctx, questionInfo) + return nil +} + +func (ip *ImporterService) NewImporterFunc() plugin.ImporterFunc { + return &ImporterFunc{importerService: ip} +} + +func (ip *ImporterService) ImportQuestion(ctx context.Context, questionInfo plugin.QuestionImporterInfo) (err error) { + req := &schema.QuestionAdd{} + errFields := make([]*validator.FormErrorField, 0) + // To limit rate, remove the following code from comment: Part 1/2 + // reject, rejectKey := ipc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) + // if reject { + // return + // } + userInfo, exist, err := ip.userCommon.GetByEmail(ctx, questionInfo.UserEmail) + if err != nil { + log.Errorf("error: %v", err) + return err + } + if !exist { + return fmt.Errorf("User not found") + } + + // To limit rate, remove the following code from comment: Part 2/2 + // defer func() { + // // If status is not 200 means that the bad request has been returned, so the record should be cleared + // if ctx.Writer.Status() != http.StatusOK { + // ipc.rateLimitMiddleware.DuplicateRequestClear(ctx, rejectKey) + // } + // }() + req.UserID = userInfo.ID + req.Title = questionInfo.Title + req.Content = questionInfo.Content + req.HTML = "

" + questionInfo.Content + "

" + req.Tags = make([]*schema.TagItem, len(questionInfo.Tags)) + for i, tag := range questionInfo.Tags { + req.Tags[i] = &schema.TagItem{ + SlugName: tag, + DisplayName: tag, + } + } + canList, requireRanks, err := ip.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{ + permission.QuestionAdd, + permission.QuestionEdit, + permission.QuestionDelete, + permission.QuestionClose, + permission.QuestionReopen, + permission.TagUseReservedTag, + permission.TagAdd, + permission.LinkUrlLimit, + }) + if err != nil { + log.Errorf("error: %v", err) + return err + } + req.CanAdd = canList[0] + req.CanEdit = canList[1] + req.CanDelete = canList[2] + req.CanClose = canList[3] + req.CanReopen = canList[4] + req.CanUseReservedTag = canList[5] + req.CanAddTag = canList[6] + if !req.CanAdd { + log.Errorf("error: %v", err) + return err + } + hasNewTag, err := ip.questionService.HasNewTag(ctx.(*gin.Context), req.Tags) + if err != nil { + log.Errorf("error: %v", err) + return err + } + if !req.CanAddTag && hasNewTag { + lang := handler.GetLang(ctx.(*gin.Context)) + msg := translator.TrWithData(lang, reason.NoEnoughRankToOperate, &schema.PermissionTrTplData{Rank: requireRanks[6]}) + log.Errorf("error: %v", msg) + return errors.BadRequest(msg) + } + + errList, err := ip.questionService.CheckAddQuestion(ctx, req) + if err != nil { + errlist, ok := errList.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + if len(errFields) > 0 { + return errors.BadRequest(reason.RequestFormatError) + } + ginCtx := ctx.(*gin.Context) + req.UserAgent = ginCtx.GetHeader("User-Agent") + req.IP = ginCtx.ClientIP() + resp, err := ip.questionService.AddQuestion(ctx, req) + if err != nil { + errlist, ok := resp.([]*validator.FormErrorField) + if ok { + errFields = append(errFields, errlist...) + } + } + + if len(errFields) > 0 { + log.Errorf("error: RequestFormatError") + return errors.BadRequest(reason.RequestFormatError) + } + log.Info("Add Question Successfully") + return nil +} diff --git a/internal/service/meta/meta_service.go b/internal/service/meta/meta_service.go index 17760e311..9e4f07410 100644 --- a/internal/service/meta/meta_service.go +++ b/internal/service/meta/meta_service.go @@ -1,75 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package meta import ( "context" + "encoding/json" + "errors" + "github.com/apache/answer/internal/service/event_queue" + "strconv" + "strings" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/segmentfault/pacman/errors" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + metacommon "github.com/apache/answer/internal/service/meta_common" + questioncommon "github.com/apache/answer/internal/service/question_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/obj" + myErrors "github.com/segmentfault/pacman/errors" ) -// MetaRepo meta repository -type MetaRepo interface { - AddMeta(ctx context.Context, meta *entity.Meta) (err error) - RemoveMeta(ctx context.Context, id int) (err error) - UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) - GetMetaByObjectIdAndKey(ctx context.Context, objectId, key string) (meta *entity.Meta, exist bool, err error) - GetMetaList(ctx context.Context, meta *entity.Meta) (metas []*entity.Meta, err error) -} - // MetaService user service type MetaService struct { - metaRepo MetaRepo + metaCommonService *metacommon.MetaCommonService + userCommon *usercommon.UserCommon + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + eventQueueService event_queue.EventQueueService } -func NewMetaService(metaRepo MetaRepo) *MetaService { +func NewMetaService( + metaCommonService *metacommon.MetaCommonService, + userCommon *usercommon.UserCommon, + answerRepo answercommon.AnswerRepo, + questionRepo questioncommon.QuestionRepo, + eventQueueService event_queue.EventQueueService, +) *MetaService { return &MetaService{ - metaRepo: metaRepo, + metaCommonService: metaCommonService, + questionRepo: questionRepo, + userCommon: userCommon, + answerRepo: answerRepo, + eventQueueService: eventQueueService, } } -// AddMeta add meta -func (ms *MetaService) AddMeta(ctx context.Context, objID, key, value string) (err error) { - meta := &entity.Meta{ - ObjectID: objID, - Key: key, - Value: value, - } - return ms.metaRepo.AddMeta(ctx, meta) -} +// GetReactionByObjectId get reaction +func (ms *MetaService) GetReactionByObjectId(ctx context.Context, req *schema.GetReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) { + reactionMeta, err := ms.metaCommonService.GetMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey) -// RemoveMeta delete meta -func (ms *MetaService) RemoveMeta(ctx context.Context, id int) (err error) { - return ms.metaRepo.RemoveMeta(ctx, id) -} + // if not exist, return nil + if err != nil { + var pacmanErr *myErrors.Error + if errors.As(err, &pacmanErr) && pacmanErr.Reason == reason.MetaObjectNotFound { + return nil, nil + } else { + return nil, err + } + } -// UpdateMeta update meta -func (ms *MetaService) UpdateMeta(ctx context.Context, metaID int, key, value string) (err error) { - meta := &entity.Meta{ - ID: metaID, - Key: key, - Value: value, + var reaction schema.ReactionsSummaryMeta + err = json.Unmarshal([]byte(reactionMeta.Value), &reaction) + if err != nil { + return nil, err } - return ms.metaRepo.UpdateMeta(ctx, meta) + return ms.convertToReactionResp(ctx, req.UserID, &reaction) } -// GetMetaByObjectIdAndKey get meta one -func (ms *MetaService) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (meta *entity.Meta, err error) { - meta, exist, err := ms.metaRepo.GetMetaByObjectIdAndKey(ctx, objectID, key) +// AddOrUpdateReaction add or update reaction +func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.UpdateReactionReq) (resp *schema.GetReactionByObjectIdResp, err error) { + // check if object exist and it's answer or question + objectType, err := obj.GetObjectTypeStrByObjectID(req.ObjectID) if err != nil { - return + return nil, err } - if !exist { - return nil, errors.BadRequest(reason.UnknownError) + var event *schema.EventMsg + if objectType == constant.AnswerObjectType { + answerInfo, exist, err := ms.answerRepo.GetAnswer(ctx, req.ObjectID) + if err != nil { + return nil, err + } + if !exist { + return nil, myErrors.BadRequest(reason.AnswerNotFound) + } + event = schema.NewEvent(constant.EventAnswerReact, req.UserID).TID(answerInfo.ID). + AID(answerInfo.ID, answerInfo.UserID) + } else if objectType == constant.QuestionObjectType { + questionInfo, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID) + if err != nil { + return nil, err + } + if !exist { + return nil, myErrors.BadRequest(reason.QuestionNotFound) + } + event = schema.NewEvent(constant.EventQuestionReact, req.UserID).TID(questionInfo.ID). + QID(questionInfo.ID, questionInfo.UserID) + } else { + return nil, myErrors.BadRequest(reason.ObjectNotFound) } - return meta, nil -} -// GetMetaList get meta list all -func (ms *MetaService) GetMetaList(ctx context.Context, objID string) (metas []*entity.Meta, err error) { - metas, err = ms.metaRepo.GetMetaList(ctx, &entity.Meta{ObjectID: objID}) + // add or update + reactions := &schema.ReactionsSummaryMeta{} + err = ms.metaCommonService.AddOrUpdateMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey, func(meta *entity.Meta, exist bool) (*entity.Meta, error) { + // if not exist, create new one + if exist { + _ = json.Unmarshal([]byte(meta.Value), reactions) + } + + // update reaction + ms.updateReaction(req, reactions) + + // write back to meta repo + reactSumBytes, err := json.Marshal(reactions) + if err != nil { + return nil, err + } + + return &entity.Meta{ + ObjectID: req.ObjectID, + Key: entity.ObjectReactSummaryKey, + Value: string(reactSumBytes), + }, nil + }) + + if err != nil { + return nil, myErrors.InternalServer(reason.DatabaseError).WithError(err) + } + + resp, err = ms.convertToReactionResp(ctx, req.UserID, reactions) if err != nil { return nil, err } - return metas, err + ms.eventQueueService.Send(ctx, event) + return resp, nil +} + +// updateReaction update reaction +func (ms *MetaService) updateReaction(req *schema.UpdateReactionReq, reactions *schema.ReactionsSummaryMeta) { + if req.Reaction == "activate" { + reactions.AddReactionSummary(req.Emoji, req.UserID) + } else if req.Reaction == "deactivate" { + reactions.RemoveReactionSummary(req.Emoji, req.UserID) + } +} + +func (ms *MetaService) convertToReactionResp(ctx context.Context, userId string, r *schema.ReactionsSummaryMeta) ( + resp *schema.GetReactionByObjectIdResp, err error) { + lang := handler.GetLangByCtx(ctx) + resp = &schema.GetReactionByObjectIdResp{ + ReactionSummary: make([]*schema.ReactionRespItem, 0), + } + // traverse map and convert to username + for _, reaction := range r.Reactions { + item := &schema.ReactionRespItem{ + Emoji: reaction.Emoji, + IsActive: r.CheckUserInReactionSummary(reaction.Emoji, userId), + } + + usernames := make([]string, 0) + userBasicInfos, err := ms.userCommon.BatchUserBasicInfoByID(ctx, reaction.UserIDs) + item.Count = len(userBasicInfos) + if err != nil { + return resp, err + } + // get username + for _, userBasicInfo := range userBasicInfos { + usernames = append(usernames, userBasicInfo.Username) + if len(usernames) == 5 && len(userBasicInfos) > 5 { + item.Tooltip = translator.TrWithData(lang, constant.ReactionTooltipLabel, map[string]string{ + "Count": strconv.Itoa(len(userBasicInfos) - 5), + "Names": strings.Join(usernames, ", "), + }) + break + } + } + if len(userBasicInfos) <= 5 { + item.Tooltip = strings.Join(usernames, ", ") + } + resp.ReactionSummary = append(resp.ReactionSummary, item) + } + + return resp, nil } diff --git a/internal/service/meta_common/meta_common_service.go b/internal/service/meta_common/meta_common_service.go new file mode 100644 index 000000000..4da04fb1d --- /dev/null +++ b/internal/service/meta_common/meta_common_service.go @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package metacommon + +import ( + "context" + + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + myErrors "github.com/segmentfault/pacman/errors" +) + +// MetaRepo meta repository +type MetaRepo interface { + AddMeta(ctx context.Context, meta *entity.Meta) (err error) + RemoveMeta(ctx context.Context, id int) (err error) + UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) + AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error + GetMetaByObjectIdAndKey(ctx context.Context, objectId, key string) (meta *entity.Meta, exist bool, err error) + GetMetaList(ctx context.Context, meta *entity.Meta) (metas []*entity.Meta, err error) +} + +// MetaCommonService user service +type MetaCommonService struct { + metaRepo MetaRepo +} + +func NewMetaCommonService(metaRepo MetaRepo) *MetaCommonService { + return &MetaCommonService{ + metaRepo: metaRepo, + } +} + +// AddMeta add meta +func (ms *MetaCommonService) AddMeta(ctx context.Context, objID, key, value string) (err error) { + meta := &entity.Meta{ + ObjectID: objID, + Key: key, + Value: value, + } + return ms.metaRepo.AddMeta(ctx, meta) +} + +// RemoveMeta delete meta +func (ms *MetaCommonService) RemoveMeta(ctx context.Context, id int) (err error) { + return ms.metaRepo.RemoveMeta(ctx, id) +} + +// UpdateMeta update meta +func (ms *MetaCommonService) UpdateMeta(ctx context.Context, metaID int, key, value string) (err error) { + meta := &entity.Meta{ + ID: metaID, + Key: key, + Value: value, + } + return ms.metaRepo.UpdateMeta(ctx, meta) +} + +func (ms *MetaCommonService) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objID, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) (err error) { + return ms.metaRepo.AddOrUpdateMetaByObjectIdAndKey(ctx, objID, key, f) +} + +// GetMetaByObjectIdAndKey get meta one +func (ms *MetaCommonService) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (meta *entity.Meta, err error) { + meta, exist, err := ms.metaRepo.GetMetaByObjectIdAndKey(ctx, objectID, key) + if err != nil { + return nil, err + } + if !exist { + return nil, myErrors.BadRequest(reason.MetaObjectNotFound) + } + return meta, nil +} + +// GetMetaList get meta list all +func (ms *MetaCommonService) GetMetaList(ctx context.Context, objID string) (metas []*entity.Meta, err error) { + metas, err = ms.metaRepo.GetMetaList(ctx, &entity.Meta{ObjectID: objID}) + if err != nil { + return nil, err + } + return metas, err +} diff --git a/internal/service/mock/siteinfo_repo_mock.go b/internal/service/mock/siteinfo_repo_mock.go new file mode 100644 index 000000000..a98ceb68c --- /dev/null +++ b/internal/service/mock/siteinfo_repo_mock.go @@ -0,0 +1,337 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ./siteinfo_service.go +// +// Generated by this command: +// +// mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "github.com/apache/answer/internal/entity" + schema "github.com/apache/answer/internal/schema" + gomock "go.uber.org/mock/gomock" +) + +// MockSiteInfoRepo is a mock of SiteInfoRepo interface. +type MockSiteInfoRepo struct { + ctrl *gomock.Controller + recorder *MockSiteInfoRepoMockRecorder + isgomock struct{} +} + +// MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo. +type MockSiteInfoRepoMockRecorder struct { + mock *MockSiteInfoRepo +} + +// NewMockSiteInfoRepo creates a new mock instance. +func NewMockSiteInfoRepo(ctrl *gomock.Controller) *MockSiteInfoRepo { + mock := &MockSiteInfoRepo{ctrl: ctrl} + mock.recorder = &MockSiteInfoRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSiteInfoRepo) EXPECT() *MockSiteInfoRepoMockRecorder { + return m.recorder +} + +// GetByType mocks base method. +func (m *MockSiteInfoRepo) GetByType(ctx context.Context, siteType string) (*entity.SiteInfo, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByType", ctx, siteType) + ret0, _ := ret[0].(*entity.SiteInfo) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetByType indicates an expected call of GetByType. +func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), ctx, siteType) +} + +// IsBrandingFileUsed mocks base method. +func (m *MockSiteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsBrandingFileUsed", ctx, filePath) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed. +func (mr *MockSiteInfoRepoMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBrandingFileUsed", reflect.TypeOf((*MockSiteInfoRepo)(nil).IsBrandingFileUsed), ctx, filePath) +} + +// SaveByType mocks base method. +func (m *MockSiteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveByType", ctx, siteType, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveByType indicates an expected call of SaveByType. +func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data) +} + +// MockSiteInfoCommonService is a mock of SiteInfoCommonService interface. +type MockSiteInfoCommonService struct { + ctrl *gomock.Controller + recorder *MockSiteInfoCommonServiceMockRecorder + isgomock struct{} +} + +// MockSiteInfoCommonServiceMockRecorder is the mock recorder for MockSiteInfoCommonService. +type MockSiteInfoCommonServiceMockRecorder struct { + mock *MockSiteInfoCommonService +} + +// NewMockSiteInfoCommonService creates a new mock instance. +func NewMockSiteInfoCommonService(ctrl *gomock.Controller) *MockSiteInfoCommonService { + mock := &MockSiteInfoCommonService{ctrl: ctrl} + mock.recorder = &MockSiteInfoCommonServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSiteInfoCommonService) EXPECT() *MockSiteInfoCommonServiceMockRecorder { + return m.recorder +} + +// FormatAvatar mocks base method. +func (m *MockSiteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FormatAvatar", ctx, originalAvatarData, email, userStatus) + ret0, _ := ret[0].(*schema.AvatarInfo) + return ret0 +} + +// FormatAvatar indicates an expected call of FormatAvatar. +func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, originalAvatarData, email, userStatus any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatAvatar), ctx, originalAvatarData, email, userStatus) +} + +// FormatListAvatar mocks base method. +func (m *MockSiteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) map[string]*schema.AvatarInfo { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FormatListAvatar", ctx, userList) + ret0, _ := ret[0].(map[string]*schema.AvatarInfo) + return ret0 +} + +// FormatListAvatar indicates an expected call of FormatListAvatar. +func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, userList any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatListAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatListAvatar), ctx, userList) +} + +// GetSiteBranding mocks base method. +func (m *MockSiteInfoCommonService) GetSiteBranding(ctx context.Context) (*schema.SiteBrandingResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteBranding", ctx) + ret0, _ := ret[0].(*schema.SiteBrandingResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteBranding indicates an expected call of GetSiteBranding. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteBranding", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteBranding), ctx) +} + +// GetSiteCustomCssHTML mocks base method. +func (m *MockSiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (*schema.SiteCustomCssHTMLResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteCustomCssHTML", ctx) + ret0, _ := ret[0].(*schema.SiteCustomCssHTMLResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteCustomCssHTML indicates an expected call of GetSiteCustomCssHTML. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteCustomCssHTML", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteCustomCssHTML), ctx) +} + +// GetSiteGeneral mocks base method. +func (m *MockSiteInfoCommonService) GetSiteGeneral(ctx context.Context) (*schema.SiteGeneralResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteGeneral", ctx) + ret0, _ := ret[0].(*schema.SiteGeneralResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteGeneral indicates an expected call of GetSiteGeneral. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteGeneral", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteGeneral), ctx) +} + +// GetSiteInfoByType mocks base method. +func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteInfoByType", ctx, siteType, resp) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetSiteInfoByType indicates an expected call of GetSiteInfoByType. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, siteType, resp any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInfoByType", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInfoByType), ctx, siteType, resp) +} + +// GetSiteInterface mocks base method. +func (m *MockSiteInfoCommonService) GetSiteInterface(ctx context.Context) (*schema.SiteInterfaceResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteInterface", ctx) + ret0, _ := ret[0].(*schema.SiteInterfaceResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteInterface indicates an expected call of GetSiteInterface. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInterface", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInterface), ctx) +} + +// GetSiteLegal mocks base method. +func (m *MockSiteInfoCommonService) GetSiteLegal(ctx context.Context) (*schema.SiteLegalResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteLegal", ctx) + ret0, _ := ret[0].(*schema.SiteLegalResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteLegal indicates an expected call of GetSiteLegal. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLegal(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLegal", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLegal), ctx) +} + +// GetSiteLogin mocks base method. +func (m *MockSiteInfoCommonService) GetSiteLogin(ctx context.Context) (*schema.SiteLoginResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteLogin", ctx) + ret0, _ := ret[0].(*schema.SiteLoginResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteLogin indicates an expected call of GetSiteLogin. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLogin", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLogin), ctx) +} + +// GetSiteSeo mocks base method. +func (m *MockSiteInfoCommonService) GetSiteSeo(ctx context.Context) (*schema.SiteSeoResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteSeo", ctx) + ret0, _ := ret[0].(*schema.SiteSeoResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteSeo indicates an expected call of GetSiteSeo. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteSeo", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSeo), ctx) +} + +// GetSiteTheme mocks base method. +func (m *MockSiteInfoCommonService) GetSiteTheme(ctx context.Context) (*schema.SiteThemeResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteTheme", ctx) + ret0, _ := ret[0].(*schema.SiteThemeResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteTheme indicates an expected call of GetSiteTheme. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTheme", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTheme), ctx) +} + +// GetSiteUsers mocks base method. +func (m *MockSiteInfoCommonService) GetSiteUsers(ctx context.Context) (*schema.SiteUsersResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteUsers", ctx) + ret0, _ := ret[0].(*schema.SiteUsersResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteUsers indicates an expected call of GetSiteUsers. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteUsers", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsers), ctx) +} + +// GetSiteWrite mocks base method. +func (m *MockSiteInfoCommonService) GetSiteWrite(ctx context.Context) (*schema.SiteWriteResp, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSiteWrite", ctx) + ret0, _ := ret[0].(*schema.SiteWriteResp) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSiteWrite indicates an expected call of GetSiteWrite. +func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteWrite", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteWrite), ctx) +} + +// IsBrandingFileUsed mocks base method. +func (m *MockSiteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsBrandingFileUsed", ctx, filePath) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsBrandingFileUsed indicates an expected call of IsBrandingFileUsed. +func (mr *MockSiteInfoCommonServiceMockRecorder) IsBrandingFileUsed(ctx, filePath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsBrandingFileUsed", reflect.TypeOf((*MockSiteInfoCommonService)(nil).IsBrandingFileUsed), ctx, filePath) +} diff --git a/internal/service/notice_queue/external_notification_queue.go b/internal/service/notice_queue/external_notification_queue.go new file mode 100644 index 000000000..6322a77ec --- /dev/null +++ b/internal/service/notice_queue/external_notification_queue.go @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notice_queue + +import ( + "context" + + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" +) + +type ExternalNotificationQueueService interface { + Send(ctx context.Context, msg *schema.ExternalNotificationMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error) +} + +type externalNotificationQueueService struct { + Queue chan *schema.ExternalNotificationMsg + Handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error +} + +func (ns *externalNotificationQueueService) Send(ctx context.Context, msg *schema.ExternalNotificationMsg) { + ns.Queue <- msg +} + +func (ns *externalNotificationQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.ExternalNotificationMsg) error) { + ns.Handler = handler +} + +func (ns *externalNotificationQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received notification %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for notification") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewNewQuestionNotificationQueueService create a new notification queue service +func NewNewQuestionNotificationQueueService() ExternalNotificationQueueService { + ns := &externalNotificationQueueService{} + ns.Queue = make(chan *schema.ExternalNotificationMsg, 128) + ns.working() + return ns +} diff --git a/internal/service/notice_queue/notice_queue.go b/internal/service/notice_queue/notice_queue.go index 2281a7eea..22b733e32 100644 --- a/internal/service/notice_queue/notice_queue.go +++ b/internal/service/notice_queue/notice_queue.go @@ -1,13 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package notice_queue import ( - "github.com/answerdev/answer/internal/schema" -) + "context" -var ( - NotificationQueue = make(chan *schema.NotificationMsg, 128) + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/log" ) -func AddNotification(msg *schema.NotificationMsg) { - NotificationQueue <- msg +type NotificationQueueService interface { + Send(ctx context.Context, msg *schema.NotificationMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.NotificationMsg) error) +} + +type notificationQueueService struct { + Queue chan *schema.NotificationMsg + Handler func(ctx context.Context, msg *schema.NotificationMsg) error +} + +func (ns *notificationQueueService) Send(ctx context.Context, msg *schema.NotificationMsg) { + ns.Queue <- msg +} + +func (ns *notificationQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.NotificationMsg) error) { + ns.Handler = handler +} + +func (ns *notificationQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received notification %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for notification") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewNotificationQueueService create a new notification queue service +func NewNotificationQueueService() NotificationQueueService { + ns := ¬ificationQueueService{} + ns.Queue = make(chan *schema.NotificationMsg, 128) + ns.working() + return ns } diff --git a/internal/service/notification/external_notification.go b/internal/service/notification/external_notification.go new file mode 100644 index 000000000..d6bdd2fb7 --- /dev/null +++ b/internal/service/notification/external_notification.go @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notification + +import ( + "context" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/internal/service/user_notification_config" + "github.com/segmentfault/pacman/log" +) + +type ExternalNotificationService struct { + data *data.Data + userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo + followRepo activity_common.FollowRepo + emailService *export.EmailService + userRepo usercommon.UserRepo + notificationQueueService notice_queue.ExternalNotificationQueueService + userExternalLoginRepo user_external_login.UserExternalLoginRepo + siteInfoService siteinfo_common.SiteInfoCommonService +} + +func NewExternalNotificationService( + data *data.Data, + userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, + followRepo activity_common.FollowRepo, + emailService *export.EmailService, + userRepo usercommon.UserRepo, + notificationQueueService notice_queue.ExternalNotificationQueueService, + userExternalLoginRepo user_external_login.UserExternalLoginRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *ExternalNotificationService { + n := &ExternalNotificationService{ + data: data, + userNotificationConfigRepo: userNotificationConfigRepo, + followRepo: followRepo, + emailService: emailService, + userRepo: userRepo, + notificationQueueService: notificationQueueService, + userExternalLoginRepo: userExternalLoginRepo, + siteInfoService: siteInfoService, + } + notificationQueueService.RegisterHandler(n.Handler) + return n +} + +func (ns *ExternalNotificationService) Handler(ctx context.Context, msg *schema.ExternalNotificationMsg) error { + log.Debugf("try to send external notification %+v", msg) + + // If receiver not set language, use site default language. + if len(msg.ReceiverLang) == 0 || msg.ReceiverLang == translator.DefaultLangOption { + if interfaceInfo, _ := ns.siteInfoService.GetSiteInterface(ctx); interfaceInfo != nil { + msg.ReceiverLang = interfaceInfo.Language + } + } + if msg.NewQuestionTemplateRawData != nil { + return ns.handleNewQuestionNotification(ctx, msg) + } + if msg.NewCommentTemplateRawData != nil { + return ns.handleNewCommentNotification(ctx, msg) + } + if msg.NewAnswerTemplateRawData != nil { + return ns.handleNewAnswerNotification(ctx, msg) + } + if msg.NewInviteAnswerTemplateRawData != nil { + return ns.handleInviteAnswerNotification(ctx, msg) + } + log.Errorf("unknown notification message: %+v", msg) + return nil +} + +func (ns *ExternalNotificationService) checkUserStatusBeforeNotification(ctx context.Context, userID string) ( + unavailable bool) { + userInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID) + if err != nil { + log.Errorf("get user %s info error: %v", userID, err) + return true + } + if !exist || userInfo.Status != entity.UserStatusAvailable { + return true + } + return false +} diff --git a/internal/service/notification/invite_answer_notification.go b/internal/service/notification/invite_answer_notification.go new file mode 100644 index 000000000..4e7c051cd --- /dev/null +++ b/internal/service/notification/invite_answer_notification.go @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notification + +import ( + "context" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +func (ns *ExternalNotificationService) handleInviteAnswerNotification(ctx context.Context, + msg *schema.ExternalNotificationMsg) error { + log.Debugf("try to send invite answer notification %+v", msg) + + notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) + if err != nil { + return err + } + if !exist { + return nil + } + channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) + for _, channel := range channels { + if !channel.Enable { + continue + } + switch channel.Key { + case constant.EmailChannel: + ns.sendInviteAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewInviteAnswerTemplateRawData) + } + } + return nil +} + +func (ns *ExternalNotificationService) sendInviteAnswerNotificationEmail(ctx context.Context, + userID, email, lang string, rawData *schema.NewInviteAnswerTemplateRawData) { + if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { + return + } + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + NotificationSources: []constant.NotificationSource{ + constant.InboxSource, + }, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, + } + + // If receiver has set language, use it to send email. + if len(lang) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang)) + } + title, body, err := ns.emailService.NewInviteAnswerTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + ns.emailService.SendAndSaveCodeWithTime( + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) +} diff --git a/internal/service/notification/new_answer_notification.go b/internal/service/notification/new_answer_notification.go new file mode 100644 index 000000000..91b7e2ae3 --- /dev/null +++ b/internal/service/notification/new_answer_notification.go @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notification + +import ( + "context" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +func (ns *ExternalNotificationService) handleNewAnswerNotification(ctx context.Context, + msg *schema.ExternalNotificationMsg) error { + log.Debugf("try to send new comment notification %+v", msg) + + notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) + if err != nil { + return err + } + if !exist { + return nil + } + channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) + for _, channel := range channels { + if !channel.Enable { + continue + } + switch channel.Key { + case constant.EmailChannel: + ns.sendNewAnswerNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewAnswerTemplateRawData) + } + } + return nil +} + +func (ns *ExternalNotificationService) sendNewAnswerNotificationEmail(ctx context.Context, + userID, email, lang string, rawData *schema.NewAnswerTemplateRawData) { + if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { + return + } + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + NotificationSources: []constant.NotificationSource{ + constant.InboxSource, + }, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, + } + + // If receiver has set language, use it to send email. + if len(lang) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang)) + } + title, body, err := ns.emailService.NewAnswerTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + ns.emailService.SendAndSaveCodeWithTime( + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) +} diff --git a/internal/service/notification/new_comment_notification.go b/internal/service/notification/new_comment_notification.go new file mode 100644 index 000000000..3ec99d13c --- /dev/null +++ b/internal/service/notification/new_comment_notification.go @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notification + +import ( + "context" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/schema" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +func (ns *ExternalNotificationService) handleNewCommentNotification(ctx context.Context, + msg *schema.ExternalNotificationMsg) error { + log.Debugf("try to send new comment notification %+v", msg) + + notificationConfig, exist, err := ns.userNotificationConfigRepo.GetByUserIDAndSource(ctx, msg.ReceiverUserID, constant.InboxSource) + if err != nil { + return err + } + if !exist { + return nil + } + channels := schema.NewNotificationChannelsFormJson(notificationConfig.Channels) + for _, channel := range channels { + if !channel.Enable { + continue + } + switch channel.Key { + case constant.EmailChannel: + ns.sendNewCommentNotificationEmail(ctx, msg.ReceiverUserID, msg.ReceiverEmail, msg.ReceiverLang, msg.NewCommentTemplateRawData) + } + } + return nil +} + +func (ns *ExternalNotificationService) sendNewCommentNotificationEmail(ctx context.Context, + userID, email, lang string, rawData *schema.NewCommentTemplateRawData) { + if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { + return + } + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + NotificationSources: []constant.NotificationSource{ + constant.InboxSource, + }, + Email: email, + UserID: userID, + SkipValidationLatestCode: true, + } + // If receiver has set language, use it to send email. + if len(lang) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(lang)) + } + title, body, err := ns.emailService.NewCommentTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + ns.emailService.SendAndSaveCodeWithTime( + ctx, userID, email, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) +} diff --git a/internal/service/notification/new_question_notification.go b/internal/service/notification/new_question_notification.go new file mode 100644 index 000000000..8911967a2 --- /dev/null +++ b/internal/service/notification/new_question_notification.go @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package notification + +import ( + "context" + "strings" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/display" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/plugin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" +) + +type NewQuestionSubscriber struct { + UserID string `json:"user_id"` + Channels schema.NotificationChannels `json:"channels"` + NotificationSource constant.NotificationSource `json:"notification_source"` +} + +func (ns *ExternalNotificationService) handleNewQuestionNotification(ctx context.Context, + msg *schema.ExternalNotificationMsg) error { + log.Debugf("try to send new question notification %+v", msg) + subscribers, err := ns.getNewQuestionSubscribers(ctx, msg) + if err != nil { + return err + } + log.Debugf("get subscribers %d for question %s", len(subscribers), msg.NewQuestionTemplateRawData.QuestionID) + + for _, subscriber := range subscribers { + for _, channel := range subscriber.Channels { + if !channel.Enable { + continue + } + switch channel.Key { + case constant.EmailChannel: + ns.sendNewQuestionNotificationEmail(ctx, subscriber.UserID, &schema.NewQuestionTemplateRawData{ + QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle, + QuestionID: msg.NewQuestionTemplateRawData.QuestionID, + UnsubscribeCode: token.GenerateToken(), + Tags: msg.NewQuestionTemplateRawData.Tags, + TagIDs: msg.NewQuestionTemplateRawData.TagIDs, + }) + } + } + } + + ns.syncNewQuestionNotificationToPlugin(ctx, msg) + return nil +} + +func (ns *ExternalNotificationService) getNewQuestionSubscribers(ctx context.Context, msg *schema.ExternalNotificationMsg) ( + subscribers []*NewQuestionSubscriber, err error) { + subscribersMapping := make(map[string]*NewQuestionSubscriber) + + // 1. get all this new question's tags followers + tagsFollowerIDs := make([]string, 0) + followerMapping := make(map[string]bool) + for _, tagID := range msg.NewQuestionTemplateRawData.TagIDs { + userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID) + if err != nil { + log.Error(err) + continue + } + for _, userID := range userIDs { + if _, ok := followerMapping[userID]; ok { + continue + } + followerMapping[userID] = true + tagsFollowerIDs = append(tagsFollowerIDs, userID) + } + } + userNotificationConfigs, err := ns.userNotificationConfigRepo.GetByUsersAndSource( + ctx, tagsFollowerIDs, constant.AllNewQuestionForFollowingTagsSource) + if err != nil { + return nil, err + } + for _, userNotificationConfig := range userNotificationConfigs { + if _, ok := subscribersMapping[userNotificationConfig.UserID]; ok { + continue + } + subscribersMapping[userNotificationConfig.UserID] = &NewQuestionSubscriber{ + UserID: userNotificationConfig.UserID, + Channels: schema.NewNotificationChannelsFormJson(userNotificationConfig.Channels), + NotificationSource: constant.AllNewQuestionForFollowingTagsSource, + } + } + log.Debugf("get %d subscribers from tags", len(subscribersMapping)) + + // 2. get all new question's followers + notificationConfigs, err := ns.userNotificationConfigRepo.GetBySource(ctx, constant.AllNewQuestionSource) + if err != nil { + return nil, err + } + for _, notificationConfig := range notificationConfigs { + if _, ok := subscribersMapping[notificationConfig.UserID]; ok { + continue + } + if ns.checkSendNewQuestionNotificationEmailLimit(ctx, notificationConfig.UserID) { + continue + } + subscribersMapping[notificationConfig.UserID] = &NewQuestionSubscriber{ + UserID: notificationConfig.UserID, + Channels: schema.NewNotificationChannelsFormJson(notificationConfig.Channels), + NotificationSource: constant.AllNewQuestionSource, + } + } + + // 3. remove question owner + delete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID) + for _, subscriber := range subscribersMapping { + subscribers = append(subscribers, subscriber) + } + log.Debugf("get %d subscribers from all new question config", len(subscribers)) + return subscribers, nil +} + +func (ns *ExternalNotificationService) checkSendNewQuestionNotificationEmailLimit(ctx context.Context, userID string) bool { + key := constant.NewQuestionNotificationLimitCacheKeyPrefix + userID + old, exist, err := ns.data.Cache.GetInt64(ctx, key) + if err != nil { + log.Error(err) + return false + } + if exist && old >= constant.NewQuestionNotificationLimitMax { + log.Debugf("%s user reach new question notification limit", userID) + return true + } + if !exist { + err = ns.data.Cache.SetInt64(ctx, key, 1, constant.NewQuestionNotificationLimitCacheTime) + } else { + _, err = ns.data.Cache.Increase(ctx, key, 1) + } + if err != nil { + log.Error(err) + } + return false +} + +func (ns *ExternalNotificationService) sendNewQuestionNotificationEmail(ctx context.Context, + userID string, rawData *schema.NewQuestionTemplateRawData) { + if unavailable := ns.checkUserStatusBeforeNotification(ctx, userID); unavailable { + return + } + userInfo, exist, err := ns.userRepo.GetByUserID(ctx, userID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Errorf("user %s not exist", userID) + return + } + // If receiver has set language, use it to send email. + if len(userInfo.Language) > 0 { + ctx = context.WithValue(ctx, constant.AcceptLanguageFlag, i18n.Language(userInfo.Language)) + } + title, body, err := ns.emailService.NewQuestionTemplate(ctx, rawData) + if err != nil { + log.Error(err) + return + } + + codeContent := &schema.EmailCodeContent{ + SourceType: schema.UnsubscribeSourceType, + Email: userInfo.EMail, + UserID: userID, + NotificationSources: []constant.NotificationSource{ + constant.AllNewQuestionSource, + constant.AllNewQuestionForFollowingTagsSource, + }, + SkipValidationLatestCode: true, + } + ns.emailService.SendAndSaveCodeWithTime( + ctx, userInfo.ID, userInfo.EMail, title, body, rawData.UnsubscribeCode, codeContent.ToJSONString(), 1*24*time.Hour) +} + +func (ns *ExternalNotificationService) syncNewQuestionNotificationToPlugin(ctx context.Context, + msg *schema.ExternalNotificationMsg) { + _ = plugin.CallNotification(func(fn plugin.Notification) error { + // 1. get all this new question's tags followers + subscribersMapping := make(map[string]plugin.NotificationType) + for _, tagID := range msg.NewQuestionTemplateRawData.TagIDs { + userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, tagID) + if err != nil { + log.Error(err) + continue + } + for _, userID := range userIDs { + subscribersMapping[userID] = plugin.NotificationNewQuestionFollowedTag + } + } + + // 2. get all new question's followers + questionSubscribers := fn.GetNewQuestionSubscribers() + for _, subscriber := range questionSubscribers { + subscribersMapping[subscriber] = plugin.NotificationNewQuestion + } + + // 3. remove question owner + delete(subscribersMapping, msg.NewQuestionTemplateRawData.QuestionAuthorUserID) + + pluginNotificationMsg := ns.newPluginQuestionNotification(ctx, msg) + + // 4. send notification + for subscriberUserID, notificationType := range subscribersMapping { + newMsg := plugin.NotificationMessage{} + _ = copier.Copy(&newMsg, pluginNotificationMsg) + newMsg.ReceiverUserID = subscriberUserID + newMsg.Type = notificationType + + if len(subscriberUserID) > 0 { + userInfo, _, _ := ns.userRepo.GetByUserID(ctx, subscriberUserID) + if userInfo != nil && len(userInfo.Language) > 0 && userInfo.Language != translator.DefaultLangOption { + newMsg.ReceiverLang = userInfo.Language + } + } + + userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, subscriberUserID) + if err != nil { + log.Errorf("get user external login info failed: %v", err) + return nil + } + if exist { + newMsg.ReceiverExternalID = userInfo.ExternalID + } + fn.Notify(newMsg) + } + return nil + }) +} + +func (ns *ExternalNotificationService) newPluginQuestionNotification( + ctx context.Context, msg *schema.ExternalNotificationMsg) (raw *plugin.NotificationMessage) { + raw = &plugin.NotificationMessage{ + ReceiverUserID: msg.ReceiverUserID, + ReceiverLang: msg.ReceiverLang, + QuestionTitle: msg.NewQuestionTemplateRawData.QuestionTitle, + QuestionTags: strings.Join(msg.NewQuestionTemplateRawData.Tags, ","), + } + siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + return raw + } + seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx) + if err != nil { + return raw + } + interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return raw + } + if len(raw.ReceiverLang) == 0 || raw.ReceiverLang == translator.DefaultLangOption { + raw.ReceiverLang = interfaceInfo.Language + } + raw.QuestionUrl = display.QuestionURL( + seoInfo.Permalink, siteInfo.SiteUrl, + msg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle) + return raw +} diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index b8a099764..598212aa1 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -1,17 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package notification import ( "context" "encoding/json" "fmt" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/schema" - notficationcommon "github.com/answerdev/answer/internal/service/notification_common" - "github.com/segmentfault/pacman/i18n" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/badge" + notficationcommon "github.com/apache/answer/internal/service/notification_common" + "github.com/apache/answer/internal/service/report_common" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/uid" + "github.com/jinzhu/copier" "github.com/segmentfault/pacman/log" ) @@ -20,53 +47,130 @@ type NotificationService struct { data *data.Data notificationRepo notficationcommon.NotificationRepo notificationCommon *notficationcommon.NotificationCommon + revisionService *revision_common.RevisionService + reportRepo report_common.ReportRepo + reviewService *review.ReviewService + userRepo usercommon.UserRepo + badgeRepo badge.BadgeRepo } func NewNotificationService( data *data.Data, notificationRepo notficationcommon.NotificationRepo, notificationCommon *notficationcommon.NotificationCommon, + revisionService *revision_common.RevisionService, + userRepo usercommon.UserRepo, + reportRepo report_common.ReportRepo, + reviewService *review.ReviewService, + badgeRepo badge.BadgeRepo, ) *NotificationService { return &NotificationService{ data: data, notificationRepo: notificationRepo, notificationCommon: notificationCommon, + revisionService: revisionService, + userRepo: userRepo, + reportRepo: reportRepo, + reviewService: reviewService, + badgeRepo: badgeRepo, } } -func (ns *NotificationService) GetRedDot(ctx context.Context, userID string) (*schema.RedDot, error) { +func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { + inboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID) + achievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID) + redBot := &schema.RedDot{} - inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, userID) - achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, userID) - inboxValue, err := ns.data.Cache.GetInt64(ctx, inboxKey) + redBot.Inbox, _, err = ns.data.Cache.GetInt64(ctx, inboxKey) + redBot.Achievement, _, err = ns.data.Cache.GetInt64(ctx, achievementKey) + + // get review amount + if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { + redBot.CanRevision = true + redBot.Revision = ns.countAllReviewAmount(ctx, req) + } + + // get badge award + redBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID) + return redBot, nil +} + +func (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) if err != nil { - redBot.Inbox = 0 - } else { - redBot.Inbox = inboxValue + log.Errorf("get badge award failed: %v", err) + return nil + } + if !exist { + return nil + } + + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + award := c.GetBadgeAward() + if award == nil { + return nil } - achievementValue, err := ns.data.Cache.GetInt64(ctx, achievementKey) + badgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID) if err != nil { - redBot.Achievement = 0 - } else { - redBot.Achievement = achievementValue + log.Errorf("get badge info failed: %v", err) + return nil } - return redBot, nil + if !exists { + return nil + } + award.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name) + award.Icon = badgeInfo.Icon + award.Level = badgeInfo.Level + return award } -func (ns *NotificationService) ClearRedDot(ctx context.Context, userID string, botTypeStr string) (*schema.RedDot, error) { - botType, ok := schema.NotificationType[botTypeStr] - if ok { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID) - err := ns.data.Cache.Del(ctx, key) +func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { + // get queue amount + if req.IsAdmin { + reviewCount, err := ns.reviewService.GetReviewPendingCount(ctx) + if err != nil { + log.Errorf("get report count failed: %v", err) + } else { + amount += reviewCount + } + } + + // get flag amount + if req.IsAdmin { + reportCount, err := ns.reportRepo.GetReportCount(ctx) if err != nil { - log.Error("ClearRedDot del cache error", err.Error()) + log.Errorf("get report count failed: %v", err) + } else { + amount += reportCount } } - return ns.GetRedDot(ctx, userID) + + // get suggestion amount + countUnreviewedRevision, err := ns.revisionService.GetUnreviewedRevisionCount(ctx, &schema.RevisionSearch{ + CanReviewQuestion: req.CanReviewQuestion, + CanReviewAnswer: req.CanReviewAnswer, + CanReviewTag: req.CanReviewTag, + UserID: req.UserID, + }) + if err != nil { + log.Errorf("get unreviewed revision count failed: %v", err) + } else { + amount += countUnreviewedRevision + } + return amount } -func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, botTypeStr string) error { - botType, ok := schema.NotificationType[botTypeStr] +func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { + _ = ns.notificationCommon.DeleteRedDot(ctx, req.UserID, schema.NotificationType[req.NotificationType]) + resp := &schema.GetRedDot{} + _ = copier.Copy(resp, req) + return ns.GetRedDot(ctx, resp) +} + +func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error { + botType, ok := schema.NotificationType[notificationType] if ok { err := ns.notificationRepo.ClearUnRead(ctx, userID, botType) if err != nil { @@ -79,49 +183,135 @@ func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, b func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error { notificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id) if err != nil { - log.Error("notificationRepo.GetById error", err.Error()) + log.Errorf("get notification failed: %v", err) return nil } - if !exist { + if !exist || notificationInfo.UserID != userID { return nil } - if notificationInfo.UserID == userID && notificationInfo.IsRead == schema.NotificationNotRead { + if notificationInfo.IsRead == schema.NotificationNotRead { err := ns.notificationRepo.ClearIDUnRead(ctx, userID, id) if err != nil { return err } } + err = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id) + if err != nil { + log.Errorf("remove badge award alert cache failed: %v", err) + } + + _ = ns.notificationCommon.DecreaseRedDot(ctx, userID, notificationInfo.Type) return nil } -func (ns *NotificationService) GetList(ctx context.Context, search *schema.NotificationSearch) ( +func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( pageModel *pager.PageModel, err error) { resp := make([]*schema.NotificationContent, 0) - searchType, ok := schema.NotificationType[search.TypeStr] + searchType, ok := schema.NotificationType[searchCond.TypeStr] if !ok { return pager.NewPageModel(0, resp), nil } - search.Type = searchType - notifications, count, err := ns.notificationRepo.SearchList(ctx, search) + searchInboxType := schema.NotificationInboxTypeAll + if searchType == schema.NotificationTypeInbox { + _, ok = schema.NotificationInboxType[searchCond.InboxTypeStr] + if ok { + searchInboxType = schema.NotificationInboxType[searchCond.InboxTypeStr] + } + } + searchCond.Type = searchType + searchCond.InboxType = searchInboxType + notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) + if err != nil { + return nil, err + } + resp, err = ns.formatNotificationPage(ctx, notifications) if err != nil { return nil, err } + return pager.NewPageModel(total, resp), nil +} + +func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) ( + resp []*schema.NotificationContent, err error) { + lang := handler.GetLangByCtx(ctx) + enableShortID := handler.GetEnableShortID(ctx) + userIDs := make([]string, 0) + userMapping := make(map[string]bool) for _, notificationInfo := range notifications { item := &schema.NotificationContent{} - err := json.Unmarshal([]byte(notificationInfo.Content), item) - if err != nil { + if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil { log.Error("NotificationContent Unmarshal Error", err.Error()) continue } - lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language) - item.NotificationAction = translator.GlobalTrans.Tr(lang, item.NotificationAction) + // If notification is downvote, the user info is not needed. + if item.NotificationAction == constant.NotificationDownVotedTheQuestion || + item.NotificationAction == constant.NotificationDownVotedTheAnswer { + item.UserInfo = nil + } + // If notification is badge, the user info is not needed and the title need to be translated. + if item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + badgeName := translator.Tr(lang, item.ObjectInfo.Title) + item.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct { + BadgeName string + }{BadgeName: badgeName}) + item.UserInfo = nil + } + item.ID = notificationInfo.ID + item.NotificationAction = translator.Tr(lang, item.NotificationAction) item.UpdateTime = notificationInfo.UpdatedAt.Unix() - if notificationInfo.IsRead == schema.NotificationRead { - item.IsRead = true + item.IsRead = notificationInfo.IsRead == schema.NotificationRead + + if enableShortID { + if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { + if item.ObjectInfo.ObjectID == answerID { + item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) + } + item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) + } + if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { + if item.ObjectInfo.ObjectID == questionID { + item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) + } + item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"]) + } + } + + if item.UserInfo != nil && !userMapping[item.UserInfo.ID] { + userIDs = append(userIDs, item.UserInfo.ID) + userMapping[item.UserInfo.ID] = true } resp = append(resp, item) } - return pager.NewPageModel(count, resp), nil + + if len(userIDs) == 0 { + return resp, nil + } + + users, err := ns.userRepo.BatchGetByID(ctx, userIDs) + if err != nil { + log.Error(err) + return resp, nil + } + userIDMapping := make(map[string]*entity.User, len(users)) + for _, user := range users { + userIDMapping[user.ID] = user + } + for _, item := range resp { + if item.UserInfo == nil { + continue + } + userInfo, ok := userIDMapping[item.UserInfo.ID] + if !ok { + continue + } + if userInfo.Status == entity.UserStatusDeleted { + item.UserInfo = &schema.UserBasicInfo{ + DisplayName: "user" + converter.DeleteUserDisplay(userInfo.ID), + Status: constant.UserDeleted, + } + } + } + return resp, nil } diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 3513d0c77..2bceb6298 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package notificationcommon import ( @@ -5,15 +24,22 @@ import ( "fmt" "time" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/data" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/notice_queue" - "github.com/answerdev/answer/internal/service/object_info" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/pkg/display" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/object_info" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" "github.com/goccy/go-json" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" @@ -22,21 +48,27 @@ import ( type NotificationRepo interface { AddNotification(ctx context.Context, notification *entity.Notification) (err error) - SearchList(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) + GetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) + CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) + DeleteNotification(ctx context.Context, userID string) (err error) + DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) } type NotificationCommon struct { - data *data.Data - notificationRepo NotificationRepo - activityRepo activity_common.ActivityRepo - followRepo activity_common.FollowRepo - userCommon *usercommon.UserCommon - objectInfoService *object_info.ObjService + data *data.Data + notificationRepo NotificationRepo + activityRepo activity_common.ActivityRepo + followRepo activity_common.FollowRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + notificationQueueService notice_queue.NotificationQueueService + userExternalLoginRepo user_external_login.UserExternalLoginRepo + siteInfoService siteinfo_common.SiteInfoCommonService } func NewNotificationCommon( @@ -46,74 +78,80 @@ func NewNotificationCommon( activityRepo activity_common.ActivityRepo, followRepo activity_common.FollowRepo, objectInfoService *object_info.ObjService, + notificationQueueService notice_queue.NotificationQueueService, + userExternalLoginRepo user_external_login.UserExternalLoginRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, ) *NotificationCommon { notification := &NotificationCommon{ - data: data, - notificationRepo: notificationRepo, - activityRepo: activityRepo, - followRepo: followRepo, - userCommon: userCommon, - objectInfoService: objectInfoService, - } - notification.HandleNotification() + data: data, + notificationRepo: notificationRepo, + activityRepo: activityRepo, + followRepo: followRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + notificationQueueService: notificationQueueService, + userExternalLoginRepo: userExternalLoginRepo, + siteInfoService: siteInfoService, + } + notificationQueueService.RegisterHandler(notification.AddNotification) return notification } -func (ns *NotificationCommon) HandleNotification() { - go func() { - for msg := range notice_queue.NotificationQueue { - log.Debugf("received notification %+v", msg) - err := ns.AddNotification(context.TODO(), msg) - if err != nil { - log.Error(err) - } - } - }() -} - // AddNotification // need set -// UserID +// LoginUserID // Type 1 inbox 2 achievement // [inbox] Activity // [achievement] Rank // ObjectInfo.Title // ObjectInfo.ObjectID // ObjectInfo.ObjectType -func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error { +func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) { + if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() { + return nil + } req := &schema.NotificationContent{ TriggerUserID: msg.TriggerUserID, ReceiverUserID: msg.ReceiverUserID, ObjectInfo: schema.ObjectInfo{ Title: msg.Title, - ObjectID: msg.ObjectID, + ObjectID: uid.DeShortID(msg.ObjectID), ObjectType: msg.ObjectType, }, NotificationAction: msg.NotificationAction, Type: msg.Type, } var questionID string // just for notify all followers - objInfo, err := ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) - if err != nil { - log.Error(err) - } else { - req.ObjectInfo.Title = objInfo.Title - questionID = objInfo.QuestionID + var objInfo *schema.SimpleObjectInfo + if msg.ObjectType == constant.BadgeAwardObjectType { + req.ObjectInfo.Title = msg.Title objectMap := make(map[string]string) - objectMap["question"] = objInfo.QuestionID - objectMap["answer"] = objInfo.AnswerID - objectMap["comment"] = objInfo.CommentID + objectMap["badge_id"] = msg.ExtraInfo["badge_id"] req.ObjectInfo.ObjectMap = objectMap + } else { + objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) + if err != nil { + log.Error(err) + return err + } else { + req.ObjectInfo.Title = objInfo.Title + questionID = objInfo.QuestionID + objectMap := make(map[string]string) + objectMap["question"] = uid.DeShortID(objInfo.QuestionID) + objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) + objectMap["comment"] = objInfo.CommentID + req.ObjectInfo.ObjectMap = objectMap + } } if msg.Type == schema.NotificationTypeAchievement { notificationInfo, exist, err := ns.notificationRepo.GetByUserIdObjectIdTypeId(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID, req.Type) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("get by user id object id type id error: %w", err) } rank, err := ns.activityRepo.GetUserIDObjectIDActivitySum(ctx, req.ReceiverUserID, req.ObjectInfo.ObjectID) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("get user id object id activity sum error: %w", err) } req.Rank = rank if exist { @@ -121,17 +159,14 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N updateContent := &schema.NotificationContent{} err := json.Unmarshal([]byte(notificationInfo.Content), updateContent) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("unmarshal notification content error: %w", err) } updateContent.Rank = rank - content, err := json.Marshal(updateContent) - if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() - } + content, _ := json.Marshal(updateContent) notificationInfo.Content = string(content) err = ns.notificationRepo.UpdateNotificationContent(ctx, notificationInfo) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("update notification content error: %w", err) } return nil } @@ -149,54 +184,158 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N userBasicInfo, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, req.TriggerUserID) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("get user basic info error: %w", err) } if !exist { - return errors.InternalServer(reason.UserNotFound).WithError(err).WithStack() + return fmt.Errorf("user not exist: %s", req.TriggerUserID) } req.UserInfo = userBasicInfo - content, err := json.Marshal(req) - if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + content, _ := json.Marshal(req) + _, ok := constant.NotificationMsgTypeMapping[req.NotificationAction] + if ok { + info.MsgType = constant.NotificationMsgTypeMapping[req.NotificationAction] } info.Content = string(content) err = ns.notificationRepo.AddNotification(ctx, info) if err != nil { - return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + return fmt.Errorf("add notification error: %w", err) } - err = ns.addRedDot(ctx, info.UserID, info.Type) + err = ns.addRedDot(ctx, info.UserID, msg.Type) if err != nil { log.Error("addRedDot Error", err.Error()) } + if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"]) + } + + go ns.SendNotificationToAllFollower(ctx, msg, questionID) + + if msg.Type == schema.NotificationTypeInbox { + ns.syncNotificationToPlugin(ctx, objInfo, msg) + } + return nil +} - go ns.SendNotificationToAllFollower(context.Background(), msg, questionID) +func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error { + var key string + if noticeType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + _, exist, err := ns.data.Cache.GetInt64(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if exist { + if _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil + } + err = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil +} + +func (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error { + var key string + if notificationType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + _, exist, err := ns.data.Cache.GetInt64(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + return nil + } + res, err := ns.data.Cache.Decrease(ctx, key, 1) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if res <= 0 { + return ns.DeleteRedDot(ctx, userID, notificationType) + } return nil } -func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, botType int) error { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID) - err := ns.data.Cache.SetInt64(ctx, key, 1, 30*24*time.Hour) //Expiration time is one month. +func (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error { + var key string + if notificationType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + err := ns.data.Cache.Del(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } +// AddBadgeAwardAlertCache add badge award alert cache +func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + c := schema.NewRedDotBadgeAwardCache() + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + +// RemoveBadgeAwardAlertCache remove badge award alert cache +func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + return nil + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.RemoveBadgeAward(notificationID) + if len(c.BadgeAwardList) == 0 { + return ns.data.Cache.Del(ctx, key) + } + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + // SendNotificationToAllFollower send notification to all followers func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg, questionID string) { - if msg.NoNeedPushAllFollow { + if msg.NoNeedPushAllFollow || len(questionID) == 0 { return } - if msg.NotificationAction != constant.UpdateQuestion && - msg.NotificationAction != constant.AnswerTheQuestion && - msg.NotificationAction != constant.UpdateAnswer && - msg.NotificationAction != constant.AdoptAnswer { + if msg.NotificationAction != constant.NotificationUpdateQuestion && + msg.NotificationAction != constant.NotificationAnswerTheQuestion && + msg.NotificationAction != constant.NotificationUpdateAnswer && + msg.NotificationAction != constant.NotificationAcceptAnswer { return } condObjectID := msg.ObjectID if len(questionID) > 0 { - condObjectID = questionID + condObjectID = uid.DeShortID(questionID) } userIDs, err := ns.followRepo.GetFollowUserIDs(ctx, condObjectID) if err != nil { @@ -210,6 +349,87 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, t.ReceiverUserID = userID t.TriggerUserID = msg.TriggerUserID t.NoNeedPushAllFollow = true - notice_queue.AddNotification(t) + ns.notificationQueueService.Send(ctx, t) + } +} + +func (ns *NotificationCommon) syncNotificationToPlugin(ctx context.Context, objInfo *schema.SimpleObjectInfo, + msg *schema.NotificationMsg) { + if objInfo == nil { + return + } + siteInfo, err := ns.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general info failed: %v", err) + return + } + seoInfo, err := ns.siteInfoService.GetSiteSeo(ctx) + if err != nil { + log.Errorf("get site seo info failed: %v", err) + return } + interfaceInfo, err := ns.siteInfoService.GetSiteInterface(ctx) + if err != nil { + log.Errorf("get site interface info failed: %v", err) + return + } + + objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID) + objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID) + pluginNotificationMsg := plugin.NotificationMessage{ + Type: plugin.NotificationType(msg.NotificationAction), + ReceiverUserID: msg.ReceiverUserID, + TriggerUserID: msg.TriggerUserID, + QuestionTitle: objInfo.Title, + } + + if len(objInfo.QuestionID) > 0 { + pluginNotificationMsg.QuestionUrl = + display.QuestionURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title) + } + if len(objInfo.AnswerID) > 0 { + pluginNotificationMsg.AnswerUrl = + display.AnswerURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID) + } + if len(objInfo.CommentID) > 0 { + pluginNotificationMsg.CommentUrl = + display.CommentURL(seoInfo.Permalink, siteInfo.SiteUrl, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID, objInfo.CommentID) + } + + if len(msg.TriggerUserID) > 0 { + triggerUser, exist, err := ns.userCommon.GetUserBasicInfoByID(ctx, msg.TriggerUserID) + if err != nil { + log.Errorf("get trigger user basic info failed: %v", err) + return + } + if exist { + pluginNotificationMsg.TriggerUserID = triggerUser.ID + pluginNotificationMsg.TriggerUserDisplayName = triggerUser.DisplayName + pluginNotificationMsg.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username) + } + } + + if len(pluginNotificationMsg.ReceiverLang) == 0 && len(msg.ReceiverUserID) > 0 { + userInfo, _, _ := ns.userCommon.GetUserBasicInfoByID(ctx, msg.ReceiverUserID) + if userInfo != nil { + pluginNotificationMsg.ReceiverLang = userInfo.Language + } + // If receiver not set language, use site default language. + if len(pluginNotificationMsg.ReceiverLang) == 0 || pluginNotificationMsg.ReceiverLang == translator.DefaultLangOption { + pluginNotificationMsg.ReceiverLang = interfaceInfo.Language + } + } + + _ = plugin.CallNotification(func(fn plugin.Notification) error { + userInfo, exist, err := ns.userExternalLoginRepo.GetByUserID(ctx, fn.Info().SlugName, msg.ReceiverUserID) + if err != nil { + log.Errorf("get user external login info failed: %v", err) + return nil + } + if exist { + pluginNotificationMsg.ReceiverExternalID = userInfo.ExternalID + } + fn.Notify(pluginNotificationMsg) + return nil + }) } diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index d0749612a..5ef438ff3 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -1,16 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package object_info import ( "context" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - "github.com/answerdev/answer/internal/service/comment_common" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/comment_common" + questioncommon "github.com/apache/answer/internal/service/question_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/obj" "github.com/segmentfault/pacman/errors" ) @@ -19,7 +39,8 @@ type ObjService struct { answerRepo answercommon.AnswerRepo questionRepo questioncommon.QuestionRepo commentRepo comment_common.CommentCommonRepo - tagRepo tagcommon.TagRepo + tagRepo tagcommon.TagCommonRepo + tagCommon *tagcommon.TagCommonService } // NewObjService new object service @@ -27,14 +48,141 @@ func NewObjService( answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo, commentRepo comment_common.CommentCommonRepo, - tagRepo tagcommon.TagRepo) *ObjService { + tagRepo tagcommon.TagCommonRepo, + tagCommon *tagcommon.TagCommonService, +) *ObjService { return &ObjService{ answerRepo: answerRepo, questionRepo: questionRepo, commentRepo: commentRepo, tagRepo: tagRepo, + tagCommon: tagCommon, } } +func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID string) (objInfo *schema.UnreviewedRevisionInfoInfo, err error) { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) + if err != nil { + return nil, err + } + switch objectType { + case constant.QuestionObjectType: + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + taglist, err := os.tagCommon.GetObjectEntityTag(ctx, objectID) + if err != nil { + return nil, err + } + os.tagCommon.TagsFormatRecommendAndReserved(ctx, taglist) + tags, err := os.tagCommon.TagFormat(ctx, taglist) + if err != nil { + return nil, err + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + CreatedAt: questionInfo.CreatedAt.Unix(), + ObjectID: questionInfo.ID, + QuestionID: questionInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: questionInfo.UserID, + Title: questionInfo.Title, + Content: questionInfo.OriginalText, + Html: questionInfo.ParsedText, + AnswerCount: questionInfo.AnswerCount, + AnswerAccepted: !checker.IsNotZeroString(questionInfo.AcceptedAnswerID), + Tags: tags, + Status: questionInfo.Status, + ShowStatus: questionInfo.Show, + } + case constant.AnswerObjectType: + answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + CreatedAt: answerInfo.CreatedAt.Unix(), + ObjectID: answerInfo.ID, + QuestionID: answerInfo.QuestionID, + AnswerID: answerInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: answerInfo.UserID, + Title: questionInfo.Title, + Content: answerInfo.OriginalText, + Html: answerInfo.ParsedText, + Status: answerInfo.Status, + AnswerAccepted: questionInfo.AcceptedAnswerID == answerInfo.ID, + } + case constant.TagObjectType: + tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + CreatedAt: tagInfo.CreatedAt.Unix(), + ObjectID: tagInfo.ID, + ObjectType: objectType, + Title: tagInfo.SlugName, + Content: tagInfo.OriginalText, + Html: tagInfo.ParsedText, + Status: tagInfo.Status, + } + case constant.CommentObjectType: + commentInfo, exist, err := os.commentRepo.GetCommentWithoutStatus(ctx, objectID) + if err != nil { + return nil, err + } + if !exist { + break + } + objInfo = &schema.UnreviewedRevisionInfoInfo{ + CreatedAt: commentInfo.CreatedAt.Unix(), + ObjectID: commentInfo.ID, + CommentID: commentInfo.ID, + ObjectType: objectType, + ObjectCreatorUserID: commentInfo.UserID, + Content: commentInfo.OriginalText, + Html: commentInfo.ParsedText, + Status: commentInfo.Status, + } + if len(commentInfo.QuestionID) > 0 { + questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) + if err != nil { + return nil, err + } + if exist { + objInfo.QuestionID = questionInfo.ID + } + answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID) + if err != nil { + return nil, err + } + if exist { + objInfo.AnswerID = answerInfo.ID + } + } + } + if objInfo == nil { + err = errors.BadRequest(reason.ObjectNotFound) + } + return objInfo, err +} // GetInfo get object simple information func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *schema.SimpleObjectInfo, err error) { @@ -52,12 +200,13 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc break } objInfo = &schema.SimpleObjectInfo{ - ObjectID: questionInfo.ID, - ObjectCreator: questionInfo.UserID, - QuestionID: questionInfo.ID, - ObjectType: objectType, - Title: questionInfo.Title, - Content: questionInfo.ParsedText, // todo trim + ObjectID: questionInfo.ID, + ObjectCreatorUserID: questionInfo.UserID, + QuestionID: questionInfo.ID, + QuestionStatus: questionInfo.Status, + ObjectType: objectType, + Title: questionInfo.Title, + Content: questionInfo.ParsedText, // todo trim } case constant.AnswerObjectType: answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID) @@ -71,14 +220,19 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc if err != nil { return nil, err } + if !exist { + break + } objInfo = &schema.SimpleObjectInfo{ - ObjectID: answerInfo.ID, - ObjectCreator: answerInfo.UserID, - QuestionID: answerInfo.QuestionID, - AnswerID: answerInfo.ID, - ObjectType: objectType, - Title: questionInfo.Title, // this should be question title - Content: answerInfo.ParsedText, // todo trim + ObjectID: answerInfo.ID, + ObjectCreatorUserID: answerInfo.UserID, + QuestionID: answerInfo.QuestionID, + QuestionStatus: questionInfo.Status, + AnswerStatus: answerInfo.Status, + AnswerID: answerInfo.ID, + ObjectType: objectType, + Title: questionInfo.Title, // this should be question title + Content: answerInfo.ParsedText, // todo trim } case constant.CommentObjectType: commentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID) @@ -89,11 +243,12 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc break } objInfo = &schema.SimpleObjectInfo{ - ObjectID: commentInfo.ID, - ObjectCreator: commentInfo.UserID, - ObjectType: objectType, - Content: commentInfo.ParsedText, // todo trim - CommentID: commentInfo.ID, + ObjectID: commentInfo.ID, + ObjectCreatorUserID: commentInfo.UserID, + ObjectType: objectType, + Content: commentInfo.ParsedText, // todo trim + CommentID: commentInfo.ID, + CommentStatus: commentInfo.Status, } if len(commentInfo.QuestionID) > 0 { questionInfo, exist, err := os.questionRepo.GetQuestion(ctx, commentInfo.QuestionID) @@ -102,6 +257,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc } if exist { objInfo.QuestionID = questionInfo.ID + objInfo.QuestionStatus = questionInfo.Status objInfo.Title = questionInfo.Title } answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID) @@ -113,7 +269,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc } } case constant.TagObjectType: - tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID) + tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true) if err != nil { return nil, err } @@ -124,7 +280,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc ObjectID: tagInfo.ID, TagID: tagInfo.ID, ObjectType: objectType, - Title: tagInfo.ParsedText, + Title: tagInfo.SlugName, Content: tagInfo.ParsedText, // todo trim } } diff --git a/internal/service/permission/answer_permission.go b/internal/service/permission/answer_permission.go new file mode 100644 index 000000000..4eb563a5a --- /dev/null +++ b/internal/service/permission/answer_permission.go @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package permission + +import ( + "context" + "github.com/apache/answer/internal/entity" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" +) + +// GetAnswerPermission get answer permission +func GetAnswerPermission(ctx context.Context, userID, creatorUserID string, + status int, canEdit, canDelete, canRecover bool) ( + actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) + actions = make([]*schema.PermissionMemberAction, 0) + if len(userID) > 0 { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "report", + Name: translator.Tr(lang, reportActionName), + Type: "reason", + }) + } + if canEdit || userID == creatorUserID { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "edit", + Name: translator.Tr(lang, editActionName), + Type: "edit", + }) + } + + if (canDelete || userID == creatorUserID) && status != entity.AnswerStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "delete", + Name: translator.Tr(lang, deleteActionName), + Type: "confirm", + }) + } + + if canRecover && status == entity.AnswerStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "undelete", + Name: translator.Tr(lang, undeleteActionName), + Type: "confirm", + }) + } + return actions +} diff --git a/internal/service/permission/comment_permission.go b/internal/service/permission/comment_permission.go index ba04e0c33..5a82cf38d 100644 --- a/internal/service/permission/comment_permission.go +++ b/internal/service/permission/comment_permission.go @@ -1,112 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package permission -import "github.com/answerdev/answer/internal/schema" +import ( + "context" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" +) -// TODO: There is currently no permission management -func GetCommentPermission(userID string, commentCreatorUserID string) ( - actions []*schema.PermissionMemberAction) { +// GetCommentPermission get comment permission +func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, + createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) actions = make([]*schema.PermissionMemberAction, 0) if len(userID) > 0 { actions = append(actions, &schema.PermissionMemberAction{ Action: "report", - Name: "Flag", + Name: translator.Tr(lang, reportActionName), Type: "reason", }) } - if userID != commentCreatorUserID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { - Action: "edit", - Name: "Edit", - Type: "edit", - }, - { - Action: "delete", - Name: "Delete", - Type: "reason", - }, - }...) - return actions -} - -func GetTagPermission(userID string, tagCreatorUserID string) ( - actions []*schema.PermissionMemberAction) { - if userID != tagCreatorUserID { - return []*schema.PermissionMemberAction{} - } - return []*schema.PermissionMemberAction{ - { + deadline := createdAt.Add(constant.CommentEditDeadline) + if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) { + actions = append(actions, &schema.PermissionMemberAction{ Action: "edit", - Name: "Edit", + Name: translator.Tr(lang, editActionName), Type: "edit", - }, - { - Action: "delete", - Name: "Delete", - Type: "reason", - }, - } -} - -func GetAnswerPermission(userID string, answerAuthID string) ( - actions []*schema.PermissionMemberAction) { - actions = make([]*schema.PermissionMemberAction, 0) - if len(userID) > 0 { - actions = append(actions, &schema.PermissionMemberAction{ - Action: "report", - Name: "Flag", - Type: "reason", }) } - if userID != answerAuthID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { - Action: "edit", - Name: "Edit", - Type: "edit", - }, - { - Action: "delete", - Name: "Delete", - Type: "confirm", - }, - }...) - return actions -} -func GetQuestionPermission(userID string, questionAuthID string) ( - actions []*schema.PermissionMemberAction) { - actions = make([]*schema.PermissionMemberAction, 0) - if len(userID) > 0 { + if canDelete || userID == creatorUserID { actions = append(actions, &schema.PermissionMemberAction{ - Action: "report", - Name: "Flag", + Action: "delete", + Name: translator.Tr(lang, deleteActionName), Type: "reason", }) } - if userID != questionAuthID { - return actions - } - actions = append(actions, []*schema.PermissionMemberAction{ - { - Action: "edit", - Name: "Edit", - Type: "edit", - }, - { - Action: "close", - Name: "Close", - Type: "confirm", - }, - { - Action: "delete", - Name: "Delete", - Type: "confirm", - }, - }...) return actions } diff --git a/internal/service/permission/permission_name.go b/internal/service/permission/permission_name.go new file mode 100644 index 000000000..fb9fbb21b --- /dev/null +++ b/internal/service/permission/permission_name.go @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package permission + +const ( + AdminAccess = "admin.access" + QuestionAdd = "question.add" + QuestionEdit = "question.edit" + QuestionEditWithoutReview = "question.edit_without_review" + QuestionDelete = "question.delete" + QuestionClose = "question.close" + QuestionReopen = "question.reopen" + QuestionVoteUp = "question.vote_up" + QuestionVoteDown = "question.vote_down" + QuestionPin = "question.pin" + QuestionUnPin = "question.unpin" + QuestionHide = "question.hide" + QuestionShow = "question.show" + AnswerAdd = "answer.add" + AnswerEdit = "answer.edit" + AnswerEditWithoutReview = "answer.edit_without_review" + AnswerDelete = "answer.delete" + AnswerAccept = "answer.accept" + AnswerVoteUp = "answer.vote_up" + AnswerVoteDown = "answer.vote_down" + AnswerInviteSomeoneToAnswer = "answer.invite_someone_to_answer" + CommentAdd = "comment.add" + CommentEdit = "comment.edit" + CommentDelete = "comment.delete" + CommentVoteUp = "comment.vote_up" + CommentVoteDown = "comment.vote_down" + ReportAdd = "report.add" + TagAdd = "tag.add" + TagEdit = "tag.edit" + TagEditSlugName = "tag.edit_slug_name" + TagEditWithoutReview = "tag.edit_without_review" + TagDelete = "tag.delete" + TagMerge = "tag.merge" + TagSynonym = "tag.synonym" + LinkUrlLimit = "link.url_limit" + VoteDetail = "vote.detail" + AnswerAudit = "answer.audit" + QuestionAudit = "question.audit" + TagAudit = "tag.audit" + TagUseReservedTag = "tag.use_reserved_tag" + AnswerUnDelete = "answer.undeleted" + QuestionUnDelete = "question.undeleted" + TagUnDelete = "tag.undeleted" +) + +const ( + reportActionName = "action.report" + editActionName = "action.edit" + deleteActionName = "action.delete" + mergeActionName = "action.merge" + undeleteActionName = "action.undelete" + closeActionName = "action.close" + reopenActionName = "action.reopen" + pinActionName = "action.pin" + unpinActionName = "action.unpin" + hideActionName = "action.hide" + showActionName = "action.show" + inviteSomeoneToAnswerActionName = "action.invite_someone_to_answer" +) diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go new file mode 100644 index 000000000..b6750beae --- /dev/null +++ b/internal/service/permission/question_permission.go @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package permission + +import ( + "context" + "github.com/apache/answer/internal/entity" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" +) + +// GetQuestionPermission get question permission +func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string, status int, + canEdit, canDelete, canClose, canReopen, canPin, canHide, canUnPin, canShow, canRecover bool) ( + actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) + actions = make([]*schema.PermissionMemberAction, 0) + if len(userID) > 0 { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "report", + Name: translator.Tr(lang, reportActionName), + Type: "reason", + }) + } + if (canEdit || userID == creatorUserID) && status != entity.QuestionStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "edit", + Name: translator.Tr(lang, editActionName), + Type: "edit", + }) + } + if canClose && status == entity.QuestionStatusAvailable { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "close", + Name: translator.Tr(lang, closeActionName), + Type: "confirm", + }) + } + if canReopen { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "reopen", + Name: translator.Tr(lang, reopenActionName), + Type: "confirm", + }) + } + if canPin { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "pin", + Name: translator.Tr(lang, pinActionName), + Type: "confirm", + }) + } + if canHide { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "hide", + Name: translator.Tr(lang, hideActionName), + Type: "confirm", + }) + } + + if canUnPin { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "unpin", + Name: translator.Tr(lang, unpinActionName), + Type: "confirm", + }) + } + + if canShow { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "show", + Name: translator.Tr(lang, showActionName), + Type: "confirm", + }) + } + + if (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "delete", + Name: translator.Tr(lang, deleteActionName), + Type: "confirm", + }) + } + + if canRecover && status == entity.QuestionStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "undelete", + Name: translator.Tr(lang, undeleteActionName), + Type: "confirm", + }) + } + return actions +} + +// GetQuestionExtendsPermission get question extends permission +func GetQuestionExtendsPermission(ctx context.Context, canInviteOtherToAnswer bool) ( + actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) + actions = make([]*schema.PermissionMemberAction, 0) + if canInviteOtherToAnswer { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "invite_other_to_answer", + Name: translator.Tr(lang, inviteSomeoneToAnswerActionName), + Type: "confirm", + }) + } + return actions +} diff --git a/internal/service/permission/tag_permission.go b/internal/service/permission/tag_permission.go new file mode 100644 index 000000000..67ac2fa08 --- /dev/null +++ b/internal/service/permission/tag_permission.go @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package permission + +import ( + "context" + + "github.com/apache/answer/internal/entity" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/schema" +) + +// GetTagPermission get tag permission +func GetTagPermission(ctx context.Context, status int, canEdit, canDelete, canMerge, canRecover bool) ( + actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) + actions = make([]*schema.PermissionMemberAction, 0) + if canEdit { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "edit", + Name: translator.Tr(lang, editActionName), + Type: "edit", + }) + } + + if canDelete && status != entity.TagStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "delete", + Name: translator.Tr(lang, deleteActionName), + Type: "reason", + }) + } + + if canMerge && status != entity.TagStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "merge", + Name: translator.Tr(lang, mergeActionName), + Type: "edit", + }) + } + + if canRecover && status == entity.QuestionStatusDeleted { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "undelete", + Name: translator.Tr(lang, undeleteActionName), + Type: "confirm", + }) + } + return actions +} + +// GetTagSynonymPermission get tag synonym permission +func GetTagSynonymPermission(ctx context.Context, canEdit bool) ( + actions []*schema.PermissionMemberAction) { + lang := handler.GetLangByCtx(ctx) + actions = make([]*schema.PermissionMemberAction, 0) + if canEdit { + actions = append(actions, &schema.PermissionMemberAction{ + Action: "edit", + Name: translator.Tr(lang, editActionName), + Type: "edit", + }) + } + return actions +} diff --git a/internal/service/plugin_common/plugin_common_service.go b/internal/service/plugin_common/plugin_common_service.go new file mode 100644 index 000000000..d3aa839b2 --- /dev/null +++ b/internal/service/plugin_common/plugin_common_service.go @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin_common + +import ( + "context" + "encoding/json" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/repo/search_sync" + + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/importer" + "github.com/apache/answer/plugin" +) + +type PluginConfigRepo interface { + SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) + GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) +} + +type PluginUserConfigRepo interface { + SaveUserPluginConfig(ctx context.Context, userID string, pluginSlugName, configValue string) (err error) + GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) ( + pluginUserConfig *entity.PluginUserConfig, exist bool, err error) + GetPluginUserConfigPage(ctx context.Context, page, pageSize int) ( + pluginUserConfigs []*entity.PluginUserConfig, total int64, err error) + DeleteUserPluginConfig(ctx context.Context, userID string) (err error) +} + +// PluginCommonService user service +type PluginCommonService struct { + configService *config.ConfigService + pluginConfigRepo PluginConfigRepo + pluginUserConfigRepo PluginUserConfigRepo + data *data.Data + importerService *importer.ImporterService +} + +// NewPluginCommonService new report service +func NewPluginCommonService( + pluginConfigRepo PluginConfigRepo, + pluginUserConfigRepo PluginUserConfigRepo, + configService *config.ConfigService, + data *data.Data, + importerService *importer.ImporterService, +) *PluginCommonService { + + p := &PluginCommonService{ + configService: configService, + pluginConfigRepo: pluginConfigRepo, + pluginUserConfigRepo: pluginUserConfigRepo, + data: data, + importerService: importerService, + } + p.initPluginData() + return p +} + +// UpdatePluginStatus update plugin status +func (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) { + content, err := plugin.StatusManager.MarshalJSON() + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err) + } + return ps.configService.UpdateConfig(ctx, constant.PluginStatus, string(content)) +} + +// UpdatePluginConfig update plugin config +func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) { + configValue, _ := json.Marshal(req.ConfigFields) + err = ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue)) + if err != nil { + return err + } + + _ = plugin.CallSearch(func(search plugin.Search) error { + if search.Info().SlugName == req.PluginSlugName { + search.RegisterSyncer(ctx, search_sync.NewPluginSyncer(ps.data)) + } + return nil + }) + _ = plugin.CallImporter(func(importer plugin.Importer) error { + importer.RegisterImporterFunc(ctx, ps.importerService.NewImporterFunc()) + return nil + }) + return nil +} + +// UpdatePluginUserConfig update plugin config +func (ps *PluginCommonService) UpdatePluginUserConfig(ctx context.Context, req *schema.UpdateUserPluginConfigReq) (err error) { + configValue, _ := json.Marshal(req.ConfigFields) + err = ps.pluginUserConfigRepo.SaveUserPluginConfig(ctx, req.UserID, req.PluginSlugName, string(configValue)) + if err != nil { + return err + } + return nil +} + +// GetUserPluginConfig get user plugin config +func (ps *PluginCommonService) GetUserPluginConfig(ctx context.Context, req *schema.GetUserPluginConfigReq) ( + configValue string, err error) { + pluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(ctx, req.UserID, req.PluginSlugName) + if err != nil { + return "", err + } + if !exist { + return "", nil + } + return pluginUserConfig.Value, nil +} + +func (ps *PluginCommonService) initPluginData() { + _ = plugin.CallKVStorage(func(k plugin.KVStorage) error { + k.SetOperator(plugin.NewKVOperator( + ps.data.DB, + ps.data.Cache, + k.Info().SlugName, + )) + return nil + }) + + // init plugin status + pluginStatus, err := ps.configService.GetStringValue(context.TODO(), constant.PluginStatus) + if err != nil { + log.Error(err) + } else { + if err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil { + log.Error(err) + } + } + + // init plugin config + pluginConfigs, err := ps.pluginConfigRepo.GetPluginConfigAll(context.Background()) + if err != nil { + log.Error(err) + } else { + for _, pluginConfig := range pluginConfigs { + err := plugin.CallConfig(func(fn plugin.Config) error { + if fn.Info().SlugName == pluginConfig.PluginSlugName { + return fn.ConfigReceiver([]byte(pluginConfig.Value)) + } + return nil + }) + if err != nil { + log.Errorf("parse plugin config failed: %s %v", pluginConfig.PluginSlugName, err) + } + } + + _ = plugin.CallCache(func(cache plugin.Cache) error { + ps.data.Cache = cache + return nil + }) + } + + // init plugin user config + plugin.RegisterGetPluginUserConfigFunc(func(userID, pluginSlugName string) []byte { + pluginUserConfig, exist, err := ps.pluginUserConfigRepo.GetPluginUserConfig(context.Background(), userID, pluginSlugName) + if err != nil { + log.Error(err) + return nil + } + if !exist { + return nil + } + return []byte(pluginUserConfig.Value) + }) + + // init plugin user config data + go func() { + page, pageSize := 1, 1000 + for { + userConfigs, _, err := ps.pluginUserConfigRepo.GetPluginUserConfigPage(context.Background(), page, pageSize) + if err != nil { + log.Error(err) + return + } + if len(userConfigs) == 0 { + return + } + for _, userConfig := range userConfigs { + err := plugin.CallUserConfig(func(fn plugin.UserConfig) error { + if fn.Info().SlugName == userConfig.PluginSlugName { + return fn.UserConfigReceiver(userConfig.UserID, []byte(userConfig.Value)) + } + return nil + }) + if err != nil { + log.Errorf("parse plugin user config failed: %s %v", userConfig.PluginSlugName, err) + } + } + page++ + } + }() +} diff --git a/internal/service/provider.go b/internal/service/provider.go index 3901766ef..4b1b64276 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -1,31 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package service import ( - "github.com/answerdev/answer/internal/service/action" - "github.com/answerdev/answer/internal/service/activity" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - "github.com/answerdev/answer/internal/service/auth" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - "github.com/answerdev/answer/internal/service/comment" - "github.com/answerdev/answer/internal/service/comment_common" - "github.com/answerdev/answer/internal/service/export" - "github.com/answerdev/answer/internal/service/follow" - "github.com/answerdev/answer/internal/service/meta" - "github.com/answerdev/answer/internal/service/notification" - notficationcommon "github.com/answerdev/answer/internal/service/notification_common" - "github.com/answerdev/answer/internal/service/object_info" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/rank" - "github.com/answerdev/answer/internal/service/reason" - "github.com/answerdev/answer/internal/service/report" - "github.com/answerdev/answer/internal/service/report_backyard" - "github.com/answerdev/answer/internal/service/report_handle_backyard" - "github.com/answerdev/answer/internal/service/revision_common" - "github.com/answerdev/answer/internal/service/tag" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/internal/service/uploader" - "github.com/answerdev/answer/internal/service/user_backyard" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/action" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/internal/service/collection" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/internal/service/dashboard" + "github.com/apache/answer/internal/service/event_queue" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/file_record" + "github.com/apache/answer/internal/service/follow" + "github.com/apache/answer/internal/service/importer" + "github.com/apache/answer/internal/service/meta" + "github.com/apache/answer/internal/service/meta_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/notification" + notficationcommon "github.com/apache/answer/internal/service/notification_common" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/internal/service/plugin_common" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/rank" + "github.com/apache/answer/internal/service/reason" + "github.com/apache/answer/internal/service/report" + "github.com/apache/answer/internal/service/report_handle" + "github.com/apache/answer/internal/service/review" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/search_parser" + "github.com/apache/answer/internal/service/siteinfo" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/internal/service/tag" + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/internal/service/uploader" + "github.com/apache/answer/internal/service/user_admin" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/internal/service/user_notification_config" "github.com/google/wire" ) @@ -34,16 +72,16 @@ var ProviderSetService = wire.NewSet( comment.NewCommentService, comment_common.NewCommentCommonService, report.NewReportService, - NewVoteService, + content.NewVoteService, tag.NewTagService, follow.NewFollowService, - NewCollectionGroupService, - NewCollectionService, + collection.NewCollectionGroupService, + collection.NewCollectionService, action.NewCaptchaService, auth.NewAuthService, - NewUserService, - NewQuestionService, - NewAnswerService, + content.NewUserService, + content.NewQuestionService, + content.NewAnswerService, export.NewEmailService, tagcommon.NewTagCommonService, usercommon.NewUserCommon, @@ -52,17 +90,42 @@ var ProviderSetService = wire.NewSet( uploader.NewUploaderService, collectioncommon.NewCollectionCommon, revision_common.NewRevisionService, - NewRevisionService, + content.NewRevisionService, rank.NewRankService, - NewSearchService, - meta.NewMetaService, + search_parser.NewSearchParser, + content.NewSearchService, + metacommon.NewMetaCommonService, object_info.NewObjService, - report_handle_backyard.NewReportHandle, - report_backyard.NewReportBackyardService, - user_backyard.NewUserBackyardService, + report_handle.NewReportHandle, + user_admin.NewUserAdminService, reason.NewReasonService, - NewSiteInfoService, + siteinfo_common.NewSiteInfoCommonService, + siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, + dashboard.NewDashboardService, + activity_common.NewActivityCommon, + activity.NewActivityService, + role.NewRoleService, + role.NewUserRoleRelService, + role.NewRolePowerRelService, + user_external_login.NewUserExternalLoginService, + user_external_login.NewUserCenterLoginService, + plugin_common.NewPluginCommonService, + config.NewConfigService, + notice_queue.NewNotificationQueueService, + activity_queue.NewActivityQueueService, + user_notification_config.NewUserNotificationConfigService, + notification.NewExternalNotificationService, + notice_queue.NewNewQuestionNotificationQueueService, + review.NewReviewService, + meta.NewMetaService, + event_queue.NewEventQueueService, + badge.NewBadgeService, + badge.NewBadgeEventService, + badge.NewBadgeAwardService, + badge.NewBadgeGroupService, + importer.NewImporterService, + file_record.NewFileRecordService, ) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index c9c105c8a..923920485 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -1,22 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package questioncommon import ( "context" "encoding/json" + "fmt" + "math" + "strings" "time" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/meta" + "github.com/apache/answer/internal/service/siteinfo_common" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/internal/service/config" + metacommon "github.com/apache/answer/internal/service/meta_common" + "github.com/apache/answer/internal/service/revision" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/uid" "github.com/segmentfault/pacman/errors" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + collectioncommon "github.com/apache/answer/internal/service/collection_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" "github.com/segmentfault/pacman/log" ) @@ -27,31 +59,54 @@ type QuestionRepo interface { UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error) GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error) GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error) - GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questions []*entity.Question, total int64, err error) - SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error) - UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error) - SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error) - UpdatePvCount(ctx context.Context, questionId string) (err error) - UpdateAnswerCount(ctx context.Context, questionId string, num int) (err error) - UpdateCollectionCount(ctx context.Context, questionId string, num int) (err error) + GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( + questionList []*entity.Question, total int64, err error) + GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) + UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) + UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) + DeletePermanentlyQuestions(ctx context.Context) (err error) + RecoverQuestion(ctx context.Context, questionID string) (err error) + UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) + GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error) + UpdatePvCount(ctx context.Context, questionID string) (err error) + UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) + UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) - CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) + AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) + GetQuestionCount(ctx context.Context) (count int64, err error) + GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) + GetResolvedQuestionCount(ctx context.Context) (count int64, err error) + GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) + SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) + RemoveAllUserQuestion(ctx context.Context, userID string) (err error) + UpdateSearch(ctx context.Context, questionID string) (err error) + LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) + GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error) + UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) + RemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error) + RecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error) + UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) + GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questions []*entity.Question, total int64, err error) } // QuestionCommon user service type QuestionCommon struct { - questionRepo QuestionRepo - answerRepo answercommon.AnswerRepo - voteRepo activity_common.VoteRepo - followCommon activity_common.FollowRepo - tagCommon *tagcommon.TagCommonService - userCommon *usercommon.UserCommon - collectionCommon *collectioncommon.CollectionCommon - AnswerCommon *answercommon.AnswerCommon - metaService *meta.MetaService - configRepo config.ConfigRepo + questionRepo QuestionRepo + answerRepo answercommon.AnswerRepo + voteRepo activity_common.VoteRepo + followCommon activity_common.FollowRepo + tagCommon *tagcommon.TagCommonService + userCommon *usercommon.UserCommon + collectionCommon *collectioncommon.CollectionCommon + AnswerCommon *answercommon.AnswerCommon + metaCommonService *metacommon.MetaCommonService + configService *config.ConfigService + activityQueueService activity_queue.ActivityQueueService + revisionRepo revision.RevisionRepo + siteInfoService siteinfo_common.SiteInfoCommonService + data *data.Data } func NewQuestionCommon(questionRepo QuestionRepo, @@ -62,173 +117,363 @@ func NewQuestionCommon(questionRepo QuestionRepo, userCommon *usercommon.UserCommon, collectionCommon *collectioncommon.CollectionCommon, answerCommon *answercommon.AnswerCommon, - metaService *meta.MetaService, - configRepo config.ConfigRepo, - + metaCommonService *metacommon.MetaCommonService, + configService *config.ConfigService, + activityQueueService activity_queue.ActivityQueueService, + revisionRepo revision.RevisionRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, + data *data.Data, ) *QuestionCommon { return &QuestionCommon{ - questionRepo: questionRepo, - answerRepo: answerRepo, - voteRepo: voteRepo, - followCommon: followCommon, - tagCommon: tagCommon, - userCommon: userCommon, - collectionCommon: collectionCommon, - AnswerCommon: answerCommon, - metaService: metaService, - configRepo: configRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + voteRepo: voteRepo, + followCommon: followCommon, + tagCommon: tagCommon, + userCommon: userCommon, + collectionCommon: collectionCommon, + AnswerCommon: answerCommon, + metaCommonService: metaCommonService, + configService: configService, + activityQueueService: activityQueueService, + revisionRepo: revisionRepo, + siteInfoService: siteInfoService, + data: data, } } -func (qs *QuestionCommon) UpdataPv(ctx context.Context, questionId string) error { - return qs.questionRepo.UpdatePvCount(ctx, questionId) +func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) { + return qs.questionRepo.GetUserQuestionCount(ctx, userID, 0) } -func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionId string, num int) error { - return qs.questionRepo.UpdateAnswerCount(ctx, questionId, num) + +func (qs *QuestionCommon) GetPersonalUserQuestionCount(ctx context.Context, loginUserID, userID string, isAdmin bool) (count int64, err error) { + show := entity.QuestionShow + if loginUserID == userID || isAdmin { + show = 0 + } + return qs.questionRepo.GetUserQuestionCount(ctx, userID, show) } -func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionId string, num int) error { - return qs.questionRepo.UpdateCollectionCount(ctx, questionId, num) + +func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error { + return qs.questionRepo.UpdatePvCount(ctx, questionID) } -func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionId, AnswerId string) error { +func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string) error { + count, err := qs.answerRepo.GetCountByQuestionID(ctx, questionID) + if err != nil { + return err + } + if count == 0 { + err = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{ + ID: questionID, + LastAnswerID: "0", + }) + if err != nil { + return err + } + } + return qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count)) +} + +func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) { + return qs.questionRepo.UpdateCollectionCount(ctx, questionID) +} + +func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, AnswerID string) error { question := &entity.Question{} - question.ID = questionId - question.AcceptedAnswerID = AnswerId + question.ID = questionID + question.AcceptedAnswerID = AnswerID return qs.questionRepo.UpdateAccepted(ctx, question) } -func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionId, AnswerId string) error { +func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, AnswerID string) error { question := &entity.Question{} - question.ID = questionId - question.LastAnswerID = AnswerId + question.ID = questionID + question.LastAnswerID = AnswerID return qs.questionRepo.UpdateLastAnswer(ctx, question) } -func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionId string) error { +func (qs *QuestionCommon) UpdatePostTime(ctx context.Context, questionID string) error { questioninfo := &entity.Question{} now := time.Now() - questioninfo.ID = questionId + questioninfo.ID = questionID questioninfo.PostUpdateTime = now return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } +func (qs *QuestionCommon) UpdatePostSetTime(ctx context.Context, questionID string, setTime time.Time) error { + questioninfo := &entity.Question{} + questioninfo.ID = questionID + questioninfo.PostUpdateTime = setTime + return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) +} -func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIds []string, loginUserID string) (map[string]*schema.QuestionInfo, error) { - list := make(map[string]*schema.QuestionInfo) - listAddTag := make([]*entity.QuestionTag, 0) - questionList, err := qs.questionRepo.FindByID(ctx, questionIds) +func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfoResp, error) { + list := make(map[string]*schema.QuestionInfoResp) + questionList, err := qs.questionRepo.FindByID(ctx, questionIDs) if err != nil { return list, err } - for _, item := range questionList { - itemAddTag := &entity.QuestionTag{} - itemAddTag.Question = *item - listAddTag = append(listAddTag, itemAddTag) - } - QuestionInfo, err := qs.ListFormat(ctx, listAddTag, loginUserID) + questions, err := qs.FormatQuestions(ctx, questionList, loginUserID) if err != nil { return list, err } - for _, item := range QuestionInfo { + for _, item := range questions { list[item.ID] = item } return list, nil } -func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUserID string) (showinfo *schema.QuestionInfo, err error) { - dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionId) +func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) { + InviteUserInfo := make([]*schema.UserBasicInfo, 0) + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { - return showinfo, err + return InviteUserInfo, err } if !has { - return showinfo, errors.BadRequest(reason.QuestionNotFound) + return InviteUserInfo, errors.NotFound(reason.QuestionNotFound) + } + //InviteUser + if dbinfo.InviteUserID != "" { + InviteUserIDs := make([]string, 0) + err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs) + if err == nil { + inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs) + if err == nil { + for _, userid := range InviteUserIDs { + _, ok := inviteUserInfoMap[userid] + if ok { + InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid]) + } + } + } + } } - showinfo = qs.ShowFormat(ctx, dbinfo) + return InviteUserInfo, nil +} - if showinfo.Status == 2 { - metainfo, err := qs.metaService.GetMetaByObjectIdAndKey(ctx, dbinfo.ID, entity.QuestionCloseReasonKey) +func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (resp *schema.QuestionInfoResp, err error) { + questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) + if err != nil { + return resp, err + } + questionInfo.ID = uid.DeShortID(questionInfo.ID) + if !has { + return resp, errors.NotFound(reason.QuestionNotFound) + } + resp = qs.ShowFormat(ctx, questionInfo) + if resp.Status == entity.QuestionStatusClosed { + metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey) if err != nil { log.Error(err) } else { - //metainfo.Value - closemsg := &schema.CloseQuestionMeta{} - err := json.Unmarshal([]byte(metainfo.Value), closemsg) + closeMsg := &schema.CloseQuestionMeta{} + err = json.Unmarshal([]byte(metaInfo.Value), closeMsg) if err != nil { log.Error("json.Unmarshal CloseQuestionMeta error", err.Error()) } else { - closeinfo := &schema.GetReportTypeResp{} - err = qs.configRepo.GetConfigById(closemsg.CloseType, closeinfo) + cfg, err := qs.configService.GetConfigByID(ctx, closeMsg.CloseType) if err != nil { log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { + reasonItem := &schema.ReasonItem{} + _ = json.Unmarshal(cfg.GetByteValue(), reasonItem) + reasonItem.Translate(cfg.Key, handler.GetLangByCtx(ctx)) operation := &schema.Operation{} - operation.Operation_Type = closeinfo.Name - operation.Operation_Description = closeinfo.Description - operation.Operation_Msg = closemsg.CloseMsg - operation.Operation_Time = metainfo.CreatedAt.Unix() - showinfo.Operation = operation + operation.Type = reasonItem.Name + operation.Description = reasonItem.Description + operation.Msg = closeMsg.CloseMsg + operation.Time = metaInfo.CreatedAt.Unix() + operation.Level = schema.OperationLevelInfo + resp.Operation = operation } - } + } + } + if resp.Status != entity.QuestionStatusDeleted { + if resp.Tags, err = qs.tagCommon.GetObjectTag(ctx, questionID); err != nil { + return resp, err + } + } else { + revisionInfo, exist, err := qs.revisionRepo.GetLastRevisionByObjectID(ctx, questionID) + if err != nil { + log.Errorf("get revision error %s", err) + } + if exist { + questionWithTagsRevision := &entity.QuestionWithTagsRevision{} + if err = json.Unmarshal([]byte(revisionInfo.Content), questionWithTagsRevision); err != nil { + log.Errorf("revision parsing error %s", err) + return resp, nil + } + for _, tag := range questionWithTagsRevision.Tags { + resp.Tags = append(resp.Tags, &schema.TagResp{ + ID: tag.ID, + SlugName: tag.SlugName, + DisplayName: tag.DisplayName, + MainTagSlugName: tag.MainTagSlugName, + Recommend: tag.Recommend, + Reserved: tag.Reserved, + }) + } } } - tagmap, err := qs.tagCommon.GetObjectTag(ctx, questionId) + userIds := make([]string, 0) + if checker.IsNotZeroString(questionInfo.UserID) { + userIds = append(userIds, questionInfo.UserID) + } + if checker.IsNotZeroString(questionInfo.LastEditUserID) { + userIds = append(userIds, questionInfo.LastEditUserID) + } + if checker.IsNotZeroString(resp.LastAnsweredUserID) { + userIds = append(userIds, resp.LastAnsweredUserID) + } + userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { - return showinfo, err + return resp, err } - showinfo.Tags = tagmap + resp.UserInfo = userInfoMap[questionInfo.UserID] + resp.UpdateUserInfo = userInfoMap[questionInfo.LastEditUserID] + resp.LastAnsweredUserInfo = userInfoMap[resp.LastAnsweredUserID] + if len(loginUserID) == 0 { + return resp, nil + } + + resp.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID) + resp.IsFollowed, _ = qs.followCommon.IsFollowed(ctx, loginUserID, questionID) - userinfo, has, err := qs.userCommon.GetUserBasicInfoByID(ctx, dbinfo.UserID) + ids, err := qs.AnswerCommon.SearchAnswerIDs(ctx, loginUserID, questionInfo.ID) if err != nil { - return showinfo, err + log.Error("AnswerFunc.SearchAnswerIDs", err) } - if has { - showinfo.UserInfo = userinfo - showinfo.UpdateUserInfo = userinfo - showinfo.LastAnsweredUserInfo = userinfo + resp.Answered = len(ids) > 0 + if resp.Answered { + resp.FirstAnswerId = ids[0] } - if loginUserID == "" { - return showinfo, nil + collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{questionInfo.ID}) + if err != nil { + return nil, err + } + if len(collectedMap) > 0 { + resp.Collected = true } + return resp, nil +} - showinfo.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionId, loginUserID) +func (qs *QuestionCommon) FormatQuestionsPage( + ctx context.Context, questionList []*entity.Question, loginUserID string, orderCond string) ( + formattedQuestions []*schema.QuestionPageResp, err error) { + formattedQuestions = make([]*schema.QuestionPageResp, 0) + questionIDs := make([]string, 0) + userIDs := make([]string, 0) + for _, questionInfo := range questionList { + t := &schema.QuestionPageResp{ + ID: questionInfo.ID, + CreatedAt: questionInfo.CreatedAt.Unix(), + Title: questionInfo.Title, + UrlTitle: htmltext.UrlTitle(questionInfo.Title), + Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240), + Status: questionInfo.Status, + ViewCount: questionInfo.ViewCount, + UniqueViewCount: questionInfo.UniqueViewCount, + VoteCount: questionInfo.VoteCount, + AnswerCount: questionInfo.AnswerCount, + CollectionCount: questionInfo.CollectionCount, + FollowCount: questionInfo.FollowCount, + AcceptedAnswerID: questionInfo.AcceptedAnswerID, + LastAnswerID: questionInfo.LastAnswerID, + Pin: questionInfo.Pin, + Show: questionInfo.Show, + Operator: &schema.QuestionPageRespOperator{ID: questionInfo.UserID}, + } - // // check is followed - isFollowed, _ := qs.followCommon.IsFollowed(loginUserID, questionId) - showinfo.IsFollowed = isFollowed + questionIDs = append(questionIDs, questionInfo.ID) + userIDs = append(userIDs, questionInfo.UserID) + haveEdited, haveAnswered := false, false + if checker.IsNotZeroString(questionInfo.LastEditUserID) { + haveEdited = true + userIDs = append(userIDs, questionInfo.LastEditUserID) + } + if checker.IsNotZeroString(questionInfo.LastAnswerID) { + haveAnswered = true - has, err = qs.AnswerCommon.SearchAnswered(ctx, loginUserID, dbinfo.ID) - if err != nil { - log.Error("AnswerFunc.SearchAnswered", err) - } - showinfo.Answered = has + answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, questionInfo.LastAnswerID) + if err == nil && exist { + if answerInfo.LastEditUserID != "0" { + t.LastAnsweredUserID = answerInfo.LastEditUserID + } else { + t.LastAnsweredUserID = answerInfo.UserID + } + t.LastAnsweredAt = answerInfo.CreatedAt + userIDs = append(userIDs, t.LastAnsweredUserID) + } + } - //login user Collected information + // The default operation is to ask questions + t.OperationType = schema.QuestionPageRespOperationTypeAsked + t.OperatedAt = questionInfo.CreatedAt.Unix() + t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.UserID} + + // If the order is active, the last operation time is the last edit or answer time if it exists + if orderCond == schema.QuestionOrderCondActive { + if haveEdited { + t.OperationType = schema.QuestionPageRespOperationTypeModified + t.OperatedAt = questionInfo.UpdatedAt.Unix() + t.Operator = &schema.QuestionPageRespOperator{ID: questionInfo.LastEditUserID} + } + if haveAnswered { + if t.LastAnsweredAt.Unix() > t.OperatedAt { + t.OperationType = schema.QuestionPageRespOperationTypeAnswered + t.OperatedAt = t.LastAnsweredAt.Unix() + t.Operator = &schema.QuestionPageRespOperator{ID: t.LastAnsweredUserID} + } + } + } + + formattedQuestions = append(formattedQuestions, t) + } - CollectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{dbinfo.ID}) + tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, questionIDs) if err != nil { - log.Error("CollectionFunc.SearchObjectCollected", err) + return formattedQuestions, err } - _, ok := CollectedMap[dbinfo.ID] - if ok { - showinfo.Collected = true + userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIDs) + if err != nil { + return formattedQuestions, err } - return showinfo, nil + for _, item := range formattedQuestions { + tags, ok := tagsMap[item.ID] + if ok { + item.Tags = tags + } else { + item.Tags = make([]*schema.TagResp, 0) + } + userInfo, ok := userInfoMap[item.Operator.ID] + if ok { + if userInfo != nil { + item.Operator.DisplayName = userInfo.DisplayName + item.Operator.Username = userInfo.Username + item.Operator.Rank = userInfo.Rank + item.Operator.Status = userInfo.Status + item.Operator.Avatar = userInfo.Avatar + } + } + } + return formattedQuestions, nil } -func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity.QuestionTag, loginUserID string) ([]*schema.QuestionInfo, error) { - list := make([]*schema.QuestionInfo, 0) +func (qs *QuestionCommon) FormatQuestions(ctx context.Context, questionList []*entity.Question, loginUserID string) ([]*schema.QuestionInfoResp, error) { + list := make([]*schema.QuestionInfoResp, 0) objectIds := make([]string, 0) userIds := make([]string, 0) for _, questionInfo := range questionList { - item := qs.ShowListFormat(ctx, questionInfo) + item := qs.ShowFormat(ctx, questionInfo) list = append(list, item) objectIds = append(objectIds, item.ID) - userIds = append(userIds, questionInfo.UserID) + userIds = append(userIds, item.UserID, item.LastEditUserID, item.LastAnsweredUserID) } tagsMap, err := qs.tagCommon.BatchGetObjectTag(ctx, objectIds) if err != nil { @@ -241,32 +486,21 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity } for _, item := range list { - _, ok := tagsMap[item.ID] - if ok { - item.Tags = tagsMap[item.ID] - } - _, ok = userInfoMap[item.UserId] - if ok { - item.UserInfo = userInfoMap[item.UserId] - item.UpdateUserInfo = userInfoMap[item.UserId] - item.LastAnsweredUserInfo = userInfoMap[item.UserId] - } + item.Tags = tagsMap[item.ID] + item.UserInfo = userInfoMap[item.UserID] + item.UpdateUserInfo = userInfoMap[item.LastEditUserID] + item.LastAnsweredUserInfo = userInfoMap[item.LastAnsweredUserID] } - if loginUserID == "" { return list, nil } - // //login user Collected information - CollectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds) + + collectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds) if err != nil { - log.Error("CollectionFunc.SearchObjectCollected", err) + return nil, err } - for _, item := range list { - _, ok := CollectedMap[item.ID] - if ok { - item.Collected = true - } + item.Collected = collectedMap[item.ID] } return list, nil } @@ -280,20 +514,27 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove if !has { return nil } + + if questionInfo.Status == entity.QuestionStatusDeleted { + return nil + } + questionInfo.Status = entity.QuestionStatusDeleted - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) + err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } - //user add question count - err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1) + userQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID) if err != nil { - log.Error("user UpdateQuestionCount error", err.Error()) + log.Error("user GetUserQuestionCount error", err.Error()) + } else { + err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) + if err != nil { + log.Error("user IncreaseQuestionCount error", err.Error()) + } } - // todo rank remove - return nil } @@ -305,8 +546,8 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu if !has { return nil } - questionInfo.Status = entity.QuestionStatusclosed - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) + questionInfo.Status = entity.QuestionStatusClosed + err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status) if err != nil { return err } @@ -315,16 +556,23 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu CloseType: req.CloseType, CloseMsg: req.CloseMsg, }) - err = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) + err = qs.metaCommonService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) if err != nil { return err } + + qs.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: questionInfo.UserID, + ObjectID: questionInfo.ID, + OriginalObjectID: questionInfo.ID, + ActivityTypeKey: constant.ActQuestionClosed, + }) return nil } // RemoveAnswer delete answer -func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) { - answerinfo, has, err := as.answerRepo.GetByID(ctx, id) +func (qs *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err error) { + answerinfo, has, err := qs.answerRepo.GetByID(ctx, id) if err != nil { return err } @@ -332,45 +580,322 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro return nil } - //user add question count + // user add question count - err = as.UpdateAnswerCount(ctx, answerinfo.QuestionID, -1) + err = qs.UpdateAnswerCount(ctx, answerinfo.QuestionID) if err != nil { log.Error("UpdateAnswerCount error", err.Error()) } - - err = as.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, -1) + userAnswerCount, err := qs.answerRepo.GetCountByUserID(ctx, answerinfo.UserID) + if err != nil { + log.Error("GetCountByUserID error", err.Error()) + } + err = qs.userCommon.UpdateAnswerCount(ctx, answerinfo.UserID, int(userAnswerCount)) if err != nil { log.Error("user UpdateAnswerCount error", err.Error()) } - return as.answerRepo.RemoveAnswer(ctx, id) + return qs.answerRepo.RemoveAnswer(ctx, id) } -func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.QuestionTag) *schema.QuestionInfo { - return qs.ShowFormat(ctx, &data.Question) +func (qs *QuestionCommon) SitemapCron(ctx context.Context) { + questionNum, err := qs.questionRepo.GetQuestionCount(ctx) + if err != nil { + log.Error(err) + return + } + if questionNum <= constant.SitemapMaxSize { + _, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum)) + if err != nil { + log.Errorf("get site map question error: %v", err) + } + return + } + + totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize))) + for i := 1; i <= totalPages; i++ { + _, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize) + if err != nil { + log.Errorf("get site map question error: %v", err) + return + } + } } -func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo { - info := schema.QuestionInfo{} +func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info interface{}) error { + infoStr, err := json.Marshal(info) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + + err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil +} + +func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { + return qs.ShowFormat(ctx, data) +} + +func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfoResp { + info := schema.QuestionInfoResp{} info.ID = data.ID + if handler.GetEnableShortID(ctx) { + info.ID = uid.EnShortID(data.ID) + } info.Title = data.Title + info.UrlTitle = htmltext.UrlTitle(data.Title) info.Content = data.OriginalText - info.Html = data.ParsedText + info.HTML = data.ParsedText info.ViewCount = data.ViewCount info.UniqueViewCount = data.UniqueViewCount info.VoteCount = data.VoteCount info.AnswerCount = data.AnswerCount info.CollectionCount = data.CollectionCount info.FollowCount = data.FollowCount - info.AcceptedAnswerId = data.AcceptedAnswerID - info.LastAnswerId = data.LastAnswerID + info.AcceptedAnswerID = data.AcceptedAnswerID + info.LastAnswerID = data.LastAnswerID info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.PostUpdateTime = data.PostUpdateTime.Unix() + if data.PostUpdateTime.Unix() < 1 { + info.PostUpdateTime = 0 + } info.QuestionUpdateTime = data.UpdatedAt.Unix() + if data.UpdatedAt.Unix() < 1 { + info.QuestionUpdateTime = 0 + } info.Status = data.Status - info.UserId = data.UserID + info.Pin = data.Pin + info.Show = data.Show + info.UserID = data.UserID + info.LastEditUserID = data.LastEditUserID + if data.LastAnswerID != "0" { + answerInfo, exist, err := qs.answerRepo.GetAnswer(ctx, data.LastAnswerID) + if err == nil && exist { + if answerInfo.LastEditUserID != "0" { + info.LastAnsweredUserID = answerInfo.LastEditUserID + } else { + info.LastAnsweredUserID = answerInfo.UserID + } + } + + } info.Tags = make([]*schema.TagResp, 0) return &info } +func (qs *QuestionCommon) ShowFormatWithTag(ctx context.Context, data *entity.QuestionWithTagsRevision) *schema.QuestionInfoResp { + info := qs.ShowFormat(ctx, &data.Question) + Tags := make([]*schema.TagResp, 0) + for _, tag := range data.Tags { + item := &schema.TagResp{} + item.SlugName = tag.SlugName + item.DisplayName = tag.DisplayName + item.Recommend = tag.Recommend + item.Reserved = tag.Reserved + Tags = append(Tags, item) + } + info.Tags = Tags + return info +} + +func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, answerID, parsedText, originalText string) (string, error) { + err := qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: uid.DeShortID(questionID), + FromAnswerID: uid.DeShortID(answerID), + }) + if err != nil { + return parsedText, err + } + // Update the number of question links that have been removed + linkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, uid.DeShortID(questionID), entity.QuestionLinkStatusDeleted) + if err != nil { + log.Errorf("get linked question ids error %v", err) + } else { + for _, id := range linkedQuestionIDs { + if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil { + log.Errorf("update question link count error %v", err) + } + } + } + + links := checker.GetQuestionLink(originalText) + if len(links) == 0 { + return parsedText, nil + } + + // get answer ids and question ids + answerIDs := make([]string, 0, len(links)) + questionIDs := make([]string, 0, len(links)) + for _, link := range links { + if link.AnswerID != "" { + answerIDs = append(answerIDs, link.AnswerID) + } + if link.QuestionID != "" { + questionIDs = append(questionIDs, link.QuestionID) + } + } + + // get answer info and build cache + answerInfoList, err := qs.answerRepo.GetByIDs(ctx, answerIDs...) + if err != nil { + return parsedText, err + } + answerCache := make(map[string]string, len(answerInfoList)) + for _, ans := range answerInfoList { + answerID := uid.DeShortID(ans.ID) + questionID := ans.QuestionID + answerCache[answerID] = questionID + } + + // get question info and build cache + questionInfoList, err := qs.questionRepo.FindByID(ctx, questionIDs) + if err != nil { + return parsedText, err + } + questionCache := make(map[string]struct{}, len(questionInfoList)) + for _, q := range questionInfoList { + questionID := uid.DeShortID(q.ID) + questionCache[questionID] = struct{}{} + } + + // process links and generate new QuestionLink + validLinks := make([]*entity.QuestionLink, 0, len(links)) + for _, link := range links { + linkQuestionID := uid.DeShortID(link.QuestionID) + linkAnswerID := uid.DeShortID(link.AnswerID) + // validate question id + if _, exists := questionCache[linkQuestionID]; linkQuestionID != "0" && !exists { + continue + } + + // validate answer id + if linkAnswerID != "0" { + linkedQuestionID, exists := answerCache[linkAnswerID] + if !exists { + continue + } + // if question id is empty, get it from answer cache + if link.QuestionID == "" { + link.QuestionID = linkedQuestionID + } + } + + // build new link + newLink := &entity.QuestionLink{ + FromQuestionID: uid.DeShortID(questionID), + FromAnswerID: uid.DeShortID(answerID), + ToQuestionID: uid.DeShortID(link.QuestionID), + ToAnswerID: uid.DeShortID(link.AnswerID), + } + // replace link in parsed text + if link.QuestionID != "" { + htmlLink := fmt.Sprintf("#%s", link.QuestionID, link.QuestionID) + parsedText = strings.ReplaceAll(parsedText, "#"+link.QuestionID, htmlLink) + } + if link.AnswerID != "" { + linkedQuestionID := answerCache[linkAnswerID] + htmlLink := fmt.Sprintf("#%s", linkedQuestionID, link.AnswerID, link.AnswerID) + parsedText = strings.ReplaceAll(parsedText, "#"+link.AnswerID, htmlLink) + newLink.ToQuestionID = uid.DeShortID(linkedQuestionID) + } + // avoid link to self + if newLink.FromQuestionID != newLink.ToQuestionID { + validLinks = append(validLinks, newLink) + } + } + + // add new links to repo + if len(validLinks) > 0 { + err = qs.questionRepo.LinkQuestion(ctx, validLinks...) + if err != nil { + return parsedText, err + } + } + + // update question linked count + for _, link := range validLinks { + if len(link.ToQuestionID) == 0 { + continue + } + if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil { + log.Errorf("update question link count error %v", err) + } + } + + return parsedText, nil +} + +// AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question +func (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context, + questionInfo *entity.Question, closeMsg string) { + questionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg) + if len(questionID) == 0 { + return + } + + linkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID) + if err != nil { + log.Errorf("get question error %s", err) + return + } + if !exist { + return + } + err = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + ToQuestionID: linkedQuestion.ID, + Status: entity.QuestionLinkStatusAvailable, + }) + if err != nil { + log.Errorf("link question error %s", err) + } +} + +func (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) { + questionInfo.ID = uid.DeShortID(questionInfo.ID) + metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey) + if err != nil { + return + } + + closeMsgMeta := &schema.CloseQuestionMeta{} + _ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta) + + linkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg) + if len(linkedQuestionID) == 0 { + return + } + err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{ + FromQuestionID: questionInfo.ID, + ToQuestionID: linkedQuestionID, + }) + if err != nil { + log.Errorf("remove question link error %s", err) + } +} + +func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) { + siteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general error %s", err) + return + } + if !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) { + return + } + // get question id from url + // the url may like: https://xxx.com/questions/D1401/xxx + // the D1401 is question id + questionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl) + questionID = strings.TrimPrefix(questionID, "/questions/") + t := strings.Split(questionID, "/") + if len(t) < 1 { + return "" + } + questionID = t[0] + questionID = uid.DeShortID(questionID) + return questionID +} diff --git a/internal/service/question_service.go b/internal/service/question_service.go deleted file mode 100644 index 881786543..000000000 --- a/internal/service/question_service.go +++ /dev/null @@ -1,639 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity" - collectioncommon "github.com/answerdev/answer/internal/service/collection_common" - "github.com/answerdev/answer/internal/service/meta" - "github.com/answerdev/answer/internal/service/notice_queue" - "github.com/answerdev/answer/internal/service/permission" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/revision_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/i18n" - "github.com/segmentfault/pacman/log" - "golang.org/x/net/context" -) - -// QuestionRepo question repository - -// QuestionService user service -type QuestionService struct { - questionRepo questioncommon.QuestionRepo - tagCommon *tagcommon.TagCommonService - questioncommon *questioncommon.QuestionCommon - userCommon *usercommon.UserCommon - revisionService *revision_common.RevisionService - metaService *meta.MetaService - collectionCommon *collectioncommon.CollectionCommon - answerActivityService *activity.AnswerActivityService -} - -func NewQuestionService( - questionRepo questioncommon.QuestionRepo, - tagCommon *tagcommon.TagCommonService, - questioncommon *questioncommon.QuestionCommon, - userCommon *usercommon.UserCommon, - revisionService *revision_common.RevisionService, - metaService *meta.MetaService, - collectionCommon *collectioncommon.CollectionCommon, - answerActivityService *activity.AnswerActivityService, -) *QuestionService { - return &QuestionService{ - questionRepo: questionRepo, - tagCommon: tagCommon, - questioncommon: questioncommon, - userCommon: userCommon, - revisionService: revisionService, - metaService: metaService, - collectionCommon: collectionCommon, - answerActivityService: answerActivityService, - } -} - -func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQuestionReq) error { - questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) - if err != nil { - return err - } - if !has { - return nil - } - questionInfo.Status = entity.QuestionStatusclosed - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) - if err != nil { - return err - } - - closeMeta, _ := json.Marshal(schema.CloseQuestionMeta{ - CloseType: req.CloseType, - CloseMsg: req.CloseMsg, - }) - err = qs.metaService.AddMeta(ctx, req.ID, entity.QuestionCloseReasonKey, string(closeMeta)) - if err != nil { - return err - } - return nil -} - -// CloseMsgList list close question condition -func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language) ( - resp []*schema.GetCloseTypeResp, err error) { - resp = make([]*schema.GetCloseTypeResp, 0) - err = json.Unmarshal([]byte(constant.QuestionCloseJson), &resp) - if err != nil { - return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() - } - for _, t := range resp { - t.Name = translator.GlobalTrans.Tr(lang, t.Name) - t.Description = translator.GlobalTrans.Tr(lang, t.Description) - } - return resp, err -} - -// AddQuestion add question -func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo *schema.QuestionInfo, err error) { - questionInfo = &schema.QuestionInfo{} - question := &entity.Question{} - now := time.Now() - question.UserID = req.UserID - question.Title = req.Title - question.OriginalText = req.Content - question.ParsedText = req.Html - question.AcceptedAnswerID = "0" - question.LastAnswerID = "0" - question.PostUpdateTime = now - question.Status = entity.QuestionStatusAvailable - question.RevisionID = "0" - question.CreatedAt = now - question.UpdatedAt = now - err = qs.questionRepo.AddQuestion(ctx, question) - if err != nil { - return - } - objectTagData := schema.TagChange{} - objectTagData.ObjectId = question.ID - objectTagData.Tags = req.Tags - objectTagData.UserID = req.UserID - err = qs.ChangeTag(ctx, &objectTagData) - if err != nil { - return - } - - revisionDTO := &schema.AddRevisionDTO{ - UserID: question.UserID, - ObjectID: question.ID, - Title: "", - } - InfoJson, _ := json.Marshal(question) - revisionDTO.Content = string(InfoJson) - err = qs.revisionService.AddRevision(ctx, revisionDTO, true) - if err != nil { - return - } - - //user add question count - err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, 1) - if err != nil { - log.Error("user IncreaseQuestionCount error", err.Error()) - } - - questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false) - return -} - -// RemoveQuestion delete question -func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) { - questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID) - if err != nil { - return err - } - if !has { - return nil - } - questionInfo.Status = entity.QuestionStatusDeleted - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) - if err != nil { - return err - } - - //user add question count - err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1) - if err != nil { - log.Error("user IncreaseQuestionCount error", err.Error()) - } - - err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) - if err != nil { - log.Errorf("user DeleteQuestion rank rollback error %s", err.Error()) - } - - return nil -} - -// UpdateQuestion update question -func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo *schema.QuestionInfo, err error) { - questionInfo = &schema.QuestionInfo{} - now := time.Now() - question := &entity.Question{} - question.UserID = req.UserID - question.Title = req.Title - question.OriginalText = req.Content - question.ParsedText = req.Html - question.ID = req.ID - question.UpdatedAt = now - dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, question.ID) - if err != nil { - return - } - if !has { - return - } - if dbinfo.UserID != req.UserID { - return - } - err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"}) - if err != nil { - return - } - objectTagData := schema.TagChange{} - objectTagData.ObjectId = question.ID - objectTagData.Tags = req.Tags - objectTagData.UserID = req.UserID - err = qs.ChangeTag(ctx, &objectTagData) - if err != nil { - return - } - - revisionDTO := &schema.AddRevisionDTO{ - UserID: question.UserID, - ObjectID: question.ID, - Title: "", - Log: req.EditSummary, - } - InfoJson, _ := json.Marshal(question) - revisionDTO.Content = string(InfoJson) - err = qs.revisionService.AddRevision(ctx, revisionDTO, true) - if err != nil { - return - } - - questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false) - return -} - -// GetQuestion get question one -func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool) (resp *schema.QuestionInfo, err error) { - question, err := qs.questioncommon.Info(ctx, id, loginUserID) - if err != nil { - return - } - if addpv { - err = qs.questioncommon.UpdataPv(ctx, id) - if err != nil { - log.Error("UpdataPv", err) - } - } - - question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserId) - return question, nil -} - -func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error { - return qs.tagCommon.ObjectChangeTag(ctx, objectTagData) -} - -func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) { - userlist := make([]*schema.UserQuestionInfo, 0) - - userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName) - if err != nil { - return userlist, 0, err - } - if !Exist { - return userlist, 0, nil - } - search := &schema.QuestionSearch{} - search.Order = order - search.Page = page - search.PageSize = pageSize - search.UserID = userinfo.ID - questionlist, count, err := qs.SearchList(ctx, search, loginUserID) - if err != nil { - return userlist, 0, err - } - for _, item := range questionlist { - info := &schema.UserQuestionInfo{} - _ = copier.Copy(info, item) - status, ok := entity.CmsQuestionSearchStatusIntToString[item.Status] - if ok { - info.Status = status - } - userlist = append(userlist, info) - } - return userlist, count, nil -} - -func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserAnswerInfo, int64, error) { - answerlist := make([]*schema.AnswerInfo, 0) - userAnswerlist := make([]*schema.UserAnswerInfo, 0) - userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName) - if err != nil { - return userAnswerlist, 0, err - } - if !Exist { - return userAnswerlist, 0, nil - } - answersearch := &entity.AnswerSearch{} - answersearch.UserID = userinfo.ID - answersearch.PageSize = pageSize - answersearch.Page = page - if order == "newest" { - answersearch.Order = entity.Answer_Search_OrderBy_Time - } else { - answersearch.Order = entity.Answer_Search_OrderBy_Default - } - questionIDs := make([]string, 0) - answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) - if err != nil { - return userAnswerlist, count, err - } - for _, item := range answerList { - answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) - answerlist = append(answerlist, answerinfo) - questionIDs = append(questionIDs, item.QuestionID) - } - questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) - if err != nil { - return userAnswerlist, count, err - } - for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] - if ok { - item.QuestionInfo = questionMaps[item.QuestionId] - } - } - for _, item := range answerlist { - info := &schema.UserAnswerInfo{} - _ = copier.Copy(info, item) - info.AnswerID = item.ID - info.QuestionID = item.QuestionId - userAnswerlist = append(userAnswerlist, info) - } - return userAnswerlist, count, nil -} - -func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, pageSize int, loginUserID string) ([]*schema.QuestionInfo, int64, error) { - list := make([]*schema.QuestionInfo, 0) - userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, loginUserID) - if err != nil { - return list, 0, err - } - if !Exist { - return list, 0, nil - } - collectionSearch := &entity.CollectionSearch{} - collectionSearch.UserID = userinfo.ID - collectionSearch.Page = page - collectionSearch.PageSize = pageSize - collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch) - if err != nil { - return list, 0, err - } - questionIDs := make([]string, 0) - for _, item := range collectionlist { - questionIDs = append(questionIDs, item.ObjectID) - } - - questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) - if err != nil { - return list, count, err - } - for _, id := range questionIDs { - _, ok := questionMaps[id] - if ok { - questionMaps[id].LastAnsweredUserInfo = nil - questionMaps[id].UpdateUserInfo = nil - questionMaps[id].Content = "" - questionMaps[id].Html = "" - list = append(list, questionMaps[id]) - } - } - - return list, count, nil -} - -func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) { - answerlist := make([]*schema.AnswerInfo, 0) - - userAnswerlist := make([]*schema.UserAnswerInfo, 0) - userQuestionlist := make([]*schema.UserQuestionInfo, 0) - - userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName) - if err != nil { - return userQuestionlist, userAnswerlist, err - } - if !Exist { - return userQuestionlist, userAnswerlist, nil - } - search := &schema.QuestionSearch{} - search.Order = "score" - search.Page = 0 - search.PageSize = 5 - search.UserID = userinfo.ID - questionlist, _, err := qs.SearchList(ctx, search, loginUserID) - if err != nil { - return userQuestionlist, userAnswerlist, err - } - answersearch := &entity.AnswerSearch{} - answersearch.UserID = userinfo.ID - answersearch.PageSize = 5 - answersearch.Order = entity.Answer_Search_OrderBy_Vote - questionIDs := make([]string, 0) - answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) - if err != nil { - return userQuestionlist, userAnswerlist, err - } - for _, item := range answerList { - answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item) - answerlist = append(answerlist, answerinfo) - questionIDs = append(questionIDs, item.QuestionID) - } - questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) - if err != nil { - return userQuestionlist, userAnswerlist, err - } - for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] - if ok { - item.QuestionInfo = questionMaps[item.QuestionId] - } - } - - for _, item := range questionlist { - info := &schema.UserQuestionInfo{} - _ = copier.Copy(info, item) - userQuestionlist = append(userQuestionlist, info) - } - - for _, item := range answerlist { - info := &schema.UserAnswerInfo{} - _ = copier.Copy(info, item) - info.AnswerID = item.ID - info.QuestionID = item.QuestionId - userAnswerlist = append(userAnswerlist, info) - } - - return userQuestionlist, userAnswerlist, nil -} - -// SearchByTitleLike -func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string, loginUserID string) ([]*schema.QuestionBaseInfo, error) { - list := make([]*schema.QuestionBaseInfo, 0) - dblist, err := qs.questionRepo.SearchByTitleLike(ctx, title) - if err != nil { - return list, err - } - for _, question := range dblist { - item := &schema.QuestionBaseInfo{} - item.ID = question.ID - item.Title = question.Title - item.ViewCount = question.ViewCount - item.AnswerCount = question.AnswerCount - item.CollectionCount = question.CollectionCount - item.FollowCount = question.FollowCount - status, ok := entity.CmsQuestionSearchStatusIntToString[question.Status] - if ok { - item.Status = status - } - if question.AcceptedAnswerID != "0" { - item.AcceptedAnswer = true - } - list = append(list, item) - } - - return list, nil -} - -// SimilarQuestion -func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) { - list := make([]*schema.QuestionInfo, 0) - questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false) - if err != nil { - return list, 0, err - } - tagNames := make([]string, 0, len(questionInfo.Tags)) - for _, tag := range questionInfo.Tags { - tagNames = append(tagNames, tag.SlugName) - } - search := &schema.QuestionSearch{} - search.Order = "frequent" - search.Page = 0 - search.PageSize = 6 - search.Tags = tagNames - return qs.SearchList(ctx, search, loginUserID) -} - -// SearchList -func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) { - if len(req.Tags) > 0 { - taginfo, err := qs.tagCommon.GetTagListByNames(ctx, req.Tags) - if err != nil { - log.Error("tagCommon.GetTagListByNames error", err) - } - for _, tag := range taginfo { - req.TagIDs = append(req.TagIDs, tag.ID) - } - } - list := make([]*schema.QuestionInfo, 0) - if req.UserName != "" { - userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.UserName) - if err != nil { - return list, 0, err - } - if !exist { - return list, 0, err - } - req.UserID = userinfo.ID - } - questionList, count, err := qs.questionRepo.SearchList(ctx, req) - if err != nil { - return list, count, err - } - list, err = qs.questioncommon.ListFormat(ctx, questionList, loginUserID) - if err != nil { - return list, count, err - } - return list, count, nil -} - -func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionID string, setStatusStr string) error { - setStatus, ok := entity.CmsQuestionSearchStatus[setStatusStr] - if !ok { - return fmt.Errorf("question status does not exist") - } - questionInfo, exist, err := qs.questionRepo.GetQuestion(ctx, questionID) - if err != nil { - return err - } - if !exist { - return errors.BadRequest(reason.QuestionNotFound) - } - questionInfo.Status = setStatus - err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo) - if err != nil { - return err - } - - if setStatus == entity.QuestionStatusDeleted { - err = qs.answerActivityService.DeleteQuestion(ctx, questionInfo.ID, questionInfo.CreatedAt, questionInfo.VoteCount) - if err != nil { - log.Errorf("admin delete question then rank rollback error %s", err.Error()) - } - } - msg := &schema.NotificationMsg{} - msg.ObjectID = questionInfo.ID - msg.Type = schema.NotificationTypeInbox - msg.ReceiverUserID = questionInfo.UserID - msg.TriggerUserID = questionInfo.UserID - msg.ObjectType = constant.QuestionObjectType - msg.NotificationAction = constant.YourQuestionWasDeleted - notice_queue.AddNotification(msg) - return nil -} - -func (qs *QuestionService) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch, loginUserID string) ([]*schema.AdminQuestionInfo, int64, error) { - list := make([]*schema.AdminQuestionInfo, 0) - - status, ok := entity.CmsQuestionSearchStatus[search.StatusStr] - if ok { - search.Status = status - } - - if search.Status == 0 { - search.Status = 1 - } - dblist, count, err := qs.questionRepo.CmsSearchList(ctx, search) - if err != nil { - return list, count, err - } - userIds := make([]string, 0) - for _, dbitem := range dblist { - item := &schema.AdminQuestionInfo{} - _ = copier.Copy(item, dbitem) - item.CreateTime = dbitem.CreatedAt.Unix() - item.UpdateTime = dbitem.PostUpdateTime.Unix() - item.EditTime = dbitem.UpdatedAt.Unix() - list = append(list, item) - userIds = append(userIds, dbitem.UserID) - } - userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) - if err != nil { - return list, count, err - } - for _, item := range list { - _, ok = userInfoMap[item.UserID] - if ok { - item.UserInfo = userInfoMap[item.UserID] - } - } - - return list, count, nil -} - -// CmsSearchList -func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *entity.CmsAnswerSearch, loginUserID string) ([]*schema.AdminAnswerInfo, int64, error) { - answerlist := make([]*schema.AdminAnswerInfo, 0) - - status, ok := entity.CmsAnswerSearchStatus[search.StatusStr] - if ok { - search.Status = status - } - - if search.Status == 0 { - search.Status = 1 - } - dblist, count, err := qs.questioncommon.AnswerCommon.CmsSearchList(ctx, search) - if err != nil { - return answerlist, count, err - } - questionIDs := make([]string, 0) - userIds := make([]string, 0) - for _, item := range dblist { - answerinfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item) - answerlist = append(answerlist, answerinfo) - questionIDs = append(questionIDs, item.QuestionID) - userIds = append(userIds, item.UserID) - } - userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds) - if err != nil { - return answerlist, count, err - } - - questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID) - if err != nil { - return answerlist, count, err - } - for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] - if ok { - item.QuestionInfo.Title = questionMaps[item.QuestionId].Title - } - _, ok = userInfoMap[item.UserId] - if ok { - item.UserInfo = userInfoMap[item.UserId] - } - } - return answerlist, count, nil -} diff --git a/internal/service/rank/rank_service.go b/internal/service/rank/rank_service.go index c632149d3..6651091a1 100644 --- a/internal/service/rank/rank_service.go +++ b/internal/service/rank/rank_service.go @@ -1,46 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package rank import ( "context" - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_type" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/object_info" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_type" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/object_info" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/internal/service/role" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) const ( - QuestionAddRank = "rank.question.add" - QuestionEditRank = "rank.question.edit" - QuestionDeleteRank = "rank.question.delete" - QuestionVoteUpRank = "rank.question.vote_up" - QuestionVoteDownRank = "rank.question.vote_down" - AnswerAddRank = "rank.answer.add" - AnswerEditRank = "rank.answer.edit" - AnswerDeleteRank = "rank.answer.delete" - AnswerAcceptRank = "rank.answer.accept" - AnswerVoteUpRank = "rank.answer.vote_up" - AnswerVoteDownRank = "rank.answer.vote_down" - CommentAddRank = "rank.comment.add" - CommentEditRank = "rank.comment.edit" - CommentDeleteRank = "rank.comment.delete" - ReportAddRank = "rank.report.add" - TagAddRank = "rank.tag.add" - TagEditRank = "rank.tag.edit" - TagDeleteRank = "rank.tag.delete" - TagSynonymRank = "rank.tag.synonym" - LinkUrlLimitRank = "rank.link.url_limit" - VoteDetailRank = "rank.vote.detail" + PermissionPrefix = "rank." ) type UserRankRepo interface { + GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) + CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error) + ChangeUserRank(ctx context.Context, session *xorm.Session, + userID string, userCurrentScore, deltaRank int) (err error) TriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error) UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error) } @@ -48,9 +59,11 @@ type UserRankRepo interface { // RankService rank service type RankService struct { userCommon *usercommon.UserCommon - configRepo config.ConfigRepo + configService *config.ConfigService userRankRepo UserRankRepo objectInfoService *object_info.ObjService + roleService *role.UserRoleRelService + rolePowerService *role.RolePowerRelService } // NewRankService new rank service @@ -58,17 +71,22 @@ func NewRankService( userCommon *usercommon.UserCommon, userRankRepo UserRankRepo, objectInfoService *object_info.ObjService, - configRepo config.ConfigRepo) *RankService { + roleService *role.UserRoleRelService, + rolePowerService *role.RolePowerRelService, + configService *config.ConfigService) *RankService { return &RankService{ userCommon: userCommon, - configRepo: configRepo, + configService: configService, userRankRepo: userRankRepo, objectInfoService: objectInfoService, + roleService: roleService, + rolePowerService: rolePowerService, } } -// CheckRankPermission check whether the user reputation meets the permission -func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, action string) (can bool, err error) { +// CheckOperationPermission verify that the user has permission +func (rs *RankService) CheckOperationPermission(ctx context.Context, userID string, action string, objectID string) ( + can bool, err error) { if len(userID) == 0 { return false, nil } @@ -81,25 +99,172 @@ func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, a if !exist { return false, nil } - currentUserRank := userInfo.Rank + powerMapping := rs.getUserPowerMapping(ctx, userID) + if powerMapping[action] { + return true, nil + } - // get the amount of rank required for the current operation - requireRank, err := rs.configRepo.GetInt(action) + if len(objectID) > 0 { + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return can, err + } + // if the user is this object creator, the user can operate this object. + if objectInfo != nil && + objectInfo.ObjectCreatorUserID == userID { + return true, nil + } + } + + can, _ = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) + return can, nil +} + +// CheckOperationPermissionsForRanks verify that the user has permission +func (rs *RankService) CheckOperationPermissionsForRanks(ctx context.Context, userID string, actions []string) ( + can []bool, requireRanks []int, err error) { + can = make([]bool, len(actions)) + requireRanks = make([]int, len(actions)) + if len(userID) == 0 { + return can, requireRanks, nil + } + + // get the rank of the current user + userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) if err != nil { - return false, err + return can, requireRanks, err + } + if !exist { + return can, requireRanks, nil } - if currentUserRank < requireRank { + powerMapping := rs.getUserPowerMapping(ctx, userID) + for idx, action := range actions { + if powerMapping[action] { + can[idx] = true + continue + } + meetRank, requireRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) + can[idx] = meetRank + requireRanks[idx] = requireRank + } + return can, requireRanks, nil +} + +// CheckOperationPermissions verify that the user has permission +func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string) ( + can []bool, err error) { + can, _, err = rs.CheckOperationPermissionsForRanks(ctx, userID, actions) + return can, err +} + +// CheckOperationObjectOwner check operation object owner +func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool { + objectID = uid.DeShortID(objectID) + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + log.Error(err) + return false + } + // if the user is this object creator, the user can operate this object. + if objectInfo != nil && + objectInfo.ObjectCreatorUserID == userID { + return true + } + return false +} + +// CheckVotePermission verify that the user has vote permission +func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) ( + can bool, needRank int, err error) { + if len(userID) == 0 || len(objectID) == 0 { + return false, 0, nil + } + + // get the rank of the current user + userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID) + if err != nil { + return can, 0, err + } + if !exist { + return can, 0, nil + } + objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID) + if err != nil { + return can, 0, err + } + action := "" + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + if voteUp { + action = permission.QuestionVoteUp + } else { + action = permission.QuestionVoteDown + } + case constant.AnswerObjectType: + if voteUp { + action = permission.AnswerVoteUp + } else { + action = permission.AnswerVoteDown + } + case constant.CommentObjectType: + if voteUp { + action = permission.CommentVoteUp + } else { + action = permission.CommentVoteDown + } + } + powerMapping := rs.getUserPowerMapping(ctx, userID) + if powerMapping[action] { + return true, 0, nil + } + can, needRank = rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action) + return can, needRank, nil +} + +// getUserPowerMapping get user power mapping +func (rs *RankService) getUserPowerMapping(ctx context.Context, userID string) (powerMapping map[string]bool) { + powerMapping = make(map[string]bool, 0) + userRole, err := rs.roleService.GetUserRole(ctx, userID) + if err != nil { + log.Error(err) + return powerMapping + } + powers, err := rs.rolePowerService.GetRolePowerList(ctx, userRole) + if err != nil { + log.Error(err) + return powerMapping + } + + for _, power := range powers { + powerMapping[power] = true + } + return powerMapping +} + +// checkUserRank verify that the user meets the prestige criteria +func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRank int, action string) ( + can bool, rank int) { + // get the amount of rank required for the current operation + requireRank, err := rs.configService.GetIntValue(ctx, action) + if err != nil { + log.Error(err) + return false, requireRank + } + if userRank < requireRank || requireRank < 0 { log.Debugf("user %s want to do action %s, but rank %d < %d", - userInfo.DisplayName, action, currentUserRank, requireRank) - return false, nil + userID, action, userRank, requireRank) + return false, requireRank } - return true, nil + return true, requireRank } -// GetRankPersonalWithPage get personal comment list page -func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) ( +// GetRankPersonalPage get personal comment list page +func (rs *RankService) GetRankPersonalPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) ( pageModel *pager.PageModel, err error) { + if plugin.RankAgentEnabled() { + return pager.NewPageModel(0, []string{}), nil + } if len(req.Username) > 0 { userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) if err != nil { @@ -118,27 +283,47 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema. if err != nil { return nil, err } - resp := make([]*schema.GetRankPersonalWithPageResp, 0) + + resp := rs.decorateRankPersonalPageResp(ctx, userRankPage) + return pager.NewPageModel(total, resp), nil +} + +func (rs *RankService) decorateRankPersonalPageResp( + ctx context.Context, userRankPage []*entity.Activity) []*schema.GetRankPersonalPageResp { + resp := make([]*schema.GetRankPersonalPageResp, 0) + lang := handler.GetLangByCtx(ctx) + for _, userRankInfo := range userRankPage { - commentResp := &schema.GetRankPersonalWithPageResp{ + if len(userRankInfo.ObjectID) == 0 || userRankInfo.ObjectID == "0" { + continue + } + objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID) + if err != nil { + log.Error(err) + continue + } + + commentResp := &schema.GetRankPersonalPageResp{ CreatedAt: userRankInfo.CreatedAt.Unix(), ObjectID: userRankInfo.ObjectID, Reputation: userRankInfo.Rank, } - if len(userRankInfo.ObjectID) > 0 { - objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID) - if err != nil { - log.Error(err) - } else { - commentResp.RankType = activity_type.Format(userRankInfo.ActivityType) - commentResp.ObjectType = objInfo.ObjectType - commentResp.Title = objInfo.Title - commentResp.Content = objInfo.Content - commentResp.QuestionID = objInfo.QuestionID - commentResp.AnswerID = objInfo.AnswerID - } + cfg, err := rs.configService.GetConfigByID(ctx, userRankInfo.ActivityType) + if err != nil { + log.Error(err) + continue + } + commentResp.RankType = translator.Tr(lang, activity_type.ActivityTypeFlagMapping[cfg.Key]) + commentResp.ObjectType = objInfo.ObjectType + commentResp.Title = objInfo.Title + commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title) + commentResp.Content = objInfo.Content + if objInfo.QuestionStatus == entity.QuestionStatusDeleted { + commentResp.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) } + commentResp.QuestionID = objInfo.QuestionID + commentResp.AnswerID = objInfo.AnswerID resp = append(resp, commentResp) } - return pager.NewPageModel(total, resp), nil + return resp } diff --git a/internal/service/reason/reason_service.go b/internal/service/reason/reason_service.go index c3d8b2c99..35245c340 100644 --- a/internal/service/reason/reason_service.go +++ b/internal/service/reason/reason_service.go @@ -1,9 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package reason import ( "context" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/reason_common" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/reason_common" ) type ReasonService struct { @@ -16,6 +36,6 @@ func NewReasonService(reasonRepo reason_common.ReasonRepo) *ReasonService { } } -func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) { - return rs.reasonRepo.ListReasons(ctx, req) +func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []*schema.ReasonItem, err error) { + return rs.reasonRepo.ListReasons(ctx, req.ObjectType, req.Action) } diff --git a/internal/service/reason_common/reason.go b/internal/service/reason_common/reason.go index e4918ee17..18d929053 100644 --- a/internal/service/reason_common/reason.go +++ b/internal/service/reason_common/reason.go @@ -1,10 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package reason_common import ( "context" - "github.com/answerdev/answer/internal/schema" + + "github.com/apache/answer/internal/schema" ) type ReasonRepo interface { - ListReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) + ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) } diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index ab57aeafa..7dcc1d689 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -1,18 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package report import ( "encoding/json" + "github.com/apache/answer/internal/service/event_queue" - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/object_info" - "github.com/answerdev/answer/internal/service/report_common" - "github.com/answerdev/answer/pkg/obj" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/object_info" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/report_common" + "github.com/apache/answer/internal/service/report_handle" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/obj" + "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/i18n" + "github.com/segmentfault/pacman/log" "golang.org/x/net/context" ) @@ -20,14 +50,37 @@ import ( type ReportService struct { reportRepo report_common.ReportRepo objectInfoService *object_info.ObjService + commonUser *usercommon.UserCommon + answerRepo answercommon.AnswerRepo + questionRepo questioncommon.QuestionRepo + commentCommonRepo comment_common.CommentCommonRepo + reportHandle *report_handle.ReportHandle + configService *config.ConfigService + eventQueueService event_queue.EventQueueService } // NewReportService new report service -func NewReportService(reportRepo report_common.ReportRepo, - objectInfoService *object_info.ObjService) *ReportService { +func NewReportService( + reportRepo report_common.ReportRepo, + objectInfoService *object_info.ObjService, + commonUser *usercommon.UserCommon, + answerRepo answercommon.AnswerRepo, + questionRepo questioncommon.QuestionRepo, + commentCommonRepo comment_common.CommentCommonRepo, + reportHandle *report_handle.ReportHandle, + configService *config.ConfigService, + eventQueueService event_queue.EventQueueService, +) *ReportService { return &ReportService{ reportRepo: reportRepo, objectInfoService: objectInfoService, + commonUser: commonUser, + answerRepo: answerRepo, + questionRepo: questionRepo, + commentCommonRepo: commentCommonRepo, + reportHandle: reportHandle, + configService: configService, + eventQueueService: eventQueueService, } } @@ -38,42 +91,158 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq return err } - // TODO this reported user id should be get by revision objInfo, err := rs.objectInfoService.GetInfo(ctx, req.ObjectID) if err != nil { return err } + if objInfo.IsDeleted() { + return errors.BadRequest(reason.NewObjectAlreadyDeleted) + } + + cf, err := rs.configService.GetConfigByID(ctx, req.ReportType) + if err != nil || cf == nil { + return errors.BadRequest(reason.ReportNotFound) + } + if cf.Key == constant.ReasonADuplicate && !checker.IsURL(req.Content) { + return errors.BadRequest(reason.InvalidURLError) + } report := &entity.Report{ UserID: req.UserID, - ReportedUserID: objInfo.ObjectCreator, + ReportedUserID: objInfo.ObjectCreatorUserID, ObjectID: req.ObjectID, ObjectType: objectTypeNumber, ReportType: req.ReportType, Content: req.Content, Status: entity.ReportStatusPending, } - return rs.reportRepo.AddReport(ctx, report) + err = rs.reportRepo.AddReport(ctx, report) + if err != nil { + return err + } + rs.sendEvent(ctx, report, objInfo) + return nil } -// GetReportTypeList get report list all -func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Language, req *schema.GetReportListReq) ( - resp []*schema.GetReportTypeResp, err error) { - resp = make([]*schema.GetReportTypeResp, 0) - switch req.Source { - case constant.QuestionObjectType: - err = json.Unmarshal([]byte(constant.QuestionReportJson), &resp) - case constant.AnswerObjectType: - err = json.Unmarshal([]byte(constant.AnswerReportJson), &resp) - case constant.CommentObjectType: - err = json.Unmarshal([]byte(constant.CommentReportJson), &resp) +// GetUnreviewedReportPostPage get unreviewed report post page +func (rs *ReportService) GetUnreviewedReportPostPage(ctx context.Context, req *schema.GetUnreviewedReportPostPageReq) ( + pageModel *pager.PageModel, err error) { + if !req.IsAdmin { + return pager.NewPageModel(0, make([]*schema.GetReportListPageResp, 0)), nil + } + lang := handler.GetLangByCtx(ctx) + reports, total, err := rs.reportRepo.GetReportListPage(ctx, &schema.GetReportListPageDTO{ + Page: req.Page, + PageSize: 1, + Status: entity.ReportStatusPending, + }) + if err != nil { + return + } + + resp := make([]*schema.GetReportListPageResp, 0) + for _, report := range reports { + info, err := rs.objectInfoService.GetUnreviewedRevisionInfo(ctx, report.ObjectID) + if err != nil { + log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) + continue + } + + r := &schema.GetReportListPageResp{ + FlagID: report.ID, + CreatedAt: info.CreatedAt, + ObjectID: info.ObjectID, + ObjectType: info.ObjectType, + QuestionID: info.QuestionID, + AnswerID: info.AnswerID, + CommentID: info.CommentID, + Title: info.Title, + UrlTitle: htmltext.UrlTitle(info.Title), + OriginalText: info.Content, + ParsedText: info.Html, + AnswerCount: info.AnswerCount, + AnswerAccepted: info.AnswerAccepted, + Tags: info.Tags, + SubmitAt: report.CreatedAt.Unix(), + ObjectStatus: info.Status, + ObjectShowStatus: info.ShowStatus, + ReasonContent: report.Content, + } + + // get user info + userInfo, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.AuthorUserInfo, userInfo) + } + + // get submitter info + submitter, exists, e := rs.commonUser.GetUserBasicInfoByID(ctx, report.ReportedUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.SubmitterUser, submitter) + } + + if report.ReportType > 0 { + r.Reason = &schema.ReasonItem{ReasonType: report.ReportType} + cf, err := rs.configService.GetConfigByID(ctx, report.ReportType) + if err != nil { + log.Error(err) + } else { + _ = json.Unmarshal([]byte(cf.Value), r.Reason) + r.Reason.Translate(cf.Key, lang) + } + } + resp = append(resp, r) } + return pager.NewPageModel(total, resp), nil +} + +// ReviewReport review report +func (rs *ReportService) ReviewReport(ctx context.Context, req *schema.ReviewReportReq) (err error) { + report, exist, err := rs.reportRepo.GetByID(ctx, req.FlagID) if err != nil { - err = errors.BadRequest(reason.UnknownError) + return err + } + if !exist { + return errors.NotFound(reason.ReportNotFound) + } + // check if handle or not + if report.Status != entity.ReportStatusPending { + return nil + } + + // ignore this report + if req.OperationType == constant.ReportOperationIgnoreReport { + return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusIgnore) } - for _, t := range resp { - t.Name = translator.GlobalTrans.Tr(lang, t.Name) - t.Description = translator.GlobalTrans.Tr(lang, t.Description) + + if err = rs.reportHandle.UpdateReportedObject(ctx, report, req); err != nil { + return + } + + return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusCompleted) +} + +func (rs *ReportService) sendEvent(ctx context.Context, + report *entity.Report, objectInfo *schema.SimpleObjectInfo) { + var event *schema.EventMsg + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + event = schema.NewEvent(constant.EventQuestionFlag, report.UserID).TID(objectInfo.QuestionID). + QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) + case constant.AnswerObjectType: + event = schema.NewEvent(constant.EventAnswerFlag, report.UserID).TID(objectInfo.AnswerID). + AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) + case constant.CommentObjectType: + event = schema.NewEvent(constant.EventCommentFlag, report.UserID).TID(objectInfo.CommentID). + CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) + default: + return } - return resp, err + rs.eventQueueService.Send(ctx, event) } diff --git a/internal/service/report_backyard/report_backyard.go b/internal/service/report_backyard/report_backyard.go deleted file mode 100644 index 20249fced..000000000 --- a/internal/service/report_backyard/report_backyard.go +++ /dev/null @@ -1,225 +0,0 @@ -package report_backyard - -import ( - "context" - "strings" - - "github.com/answerdev/answer/internal/service/config" - - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/repo/common" - "github.com/answerdev/answer/internal/schema" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - "github.com/answerdev/answer/internal/service/comment_common" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/report_common" - "github.com/answerdev/answer/internal/service/report_handle_backyard" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" -) - -// ReportBackyardService user service -type ReportBackyardService struct { - reportRepo report_common.ReportRepo - commonUser *usercommon.UserCommon - commonRepo *common.CommonRepo - answerRepo answercommon.AnswerRepo - questionRepo questioncommon.QuestionRepo - commentCommonRepo comment_common.CommentCommonRepo - reportHandle *report_handle_backyard.ReportHandle - configRepo config.ConfigRepo -} - -// NewReportBackyardService new report service -func NewReportBackyardService( - reportRepo report_common.ReportRepo, - commonUser *usercommon.UserCommon, - commonRepo *common.CommonRepo, - answerRepo answercommon.AnswerRepo, - questionRepo questioncommon.QuestionRepo, - commentCommonRepo comment_common.CommentCommonRepo, - reportHandle *report_handle_backyard.ReportHandle, - configRepo config.ConfigRepo) *ReportBackyardService { - return &ReportBackyardService{ - reportRepo: reportRepo, - commonUser: commonUser, - commonRepo: commonRepo, - answerRepo: answerRepo, - questionRepo: questionRepo, - commentCommonRepo: commentCommonRepo, - reportHandle: reportHandle, - configRepo: configRepo, - } -} - -// ListReportPage list report pages -func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.GetReportListPageDTO) (pageModel *pager.PageModel, err error) { - var ( - resp []*schema.GetReportListPageResp - flags []entity.Report - total int64 - - flaggedUserIds, - userIds []string - - flaggedUsers, - users map[string]*schema.UserBasicInfo - ) - - pageModel = &pager.PageModel{} - - flags, total, err = rs.reportRepo.GetReportListPage(ctx, dto) - if err != nil { - return - } - - _ = copier.Copy(&resp, flags) - for _, r := range resp { - flaggedUserIds = append(flaggedUserIds, r.ReportedUserID) - userIds = append(userIds, r.UserID) - r.Format() - } - - // flagged users - flaggedUsers, err = rs.commonUser.BatchUserBasicInfoByID(ctx, flaggedUserIds) - - // flag users - users, err = rs.commonUser.BatchUserBasicInfoByID(ctx, userIds) - for _, r := range resp { - r.ReportedUser = flaggedUsers[r.ReportedUserID] - r.ReportUser = users[r.UserID] - } - - rs.parseObject(ctx, &resp) - return pager.NewPageModel(total, resp), nil -} - -// HandleReported handle the reported object -func (rs *ReportBackyardService) HandleReported(ctx context.Context, req schema.ReportHandleReq) (err error) { - var ( - reported = entity.Report{} - handleData = entity.Report{ - FlaggedContent: req.FlaggedContent, - FlaggedType: req.FlaggedType, - Status: entity.ReportStatusCompleted, - } - exist = false - ) - - reported, exist, err = rs.reportRepo.GetByID(ctx, req.ID) - if err != nil { - err = errors.BadRequest(reason.ReportHandleFailed).WithError(err).WithStack() - return - } - if !exist { - err = errors.NotFound(reason.ReportNotFound) - return - } - - // check if handle or not - if reported.Status != entity.ReportStatusPending { - return - } - - if err = rs.reportHandle.HandleObject(ctx, reported, req); err != nil { - return - } - - err = rs.reportRepo.UpdateByID(ctx, reported.ID, handleData) - return -} - -func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schema.GetReportListPageResp) { - var ( - res = *resp - ) - - for i, r := range res { - var ( - objIds map[string]string - exists, - ok bool - err error - questionId, - answerId, - commentId string - question *entity.Question - answer *entity.Answer - cmt *entity.Comment - ) - - objIds, err = rs.commonRepo.GetObjectIDMap(r.ObjectID) - if err != nil { - continue - } - - questionId, ok = objIds["question"] - if !ok { - continue - } - - question, exists, err = rs.questionRepo.GetQuestion(ctx, questionId) - if err != nil || !exists { - continue - } - - answerId, ok = objIds["answer"] - if ok { - answer, _, err = rs.answerRepo.GetAnswer(ctx, answerId) - } - - commentId, ok = objIds["comment"] - if ok { - cmt, _, err = rs.commentCommonRepo.GetComment(ctx, commentId) - } - - switch r.OType { - case "question": - r.QuestionID = questionId - r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(question.OriginalText) - - case "answer": - r.QuestionID = questionId - r.AnswerID = answerId - r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText) - - case "comment": - r.QuestionID = questionId - r.AnswerID = answerId - r.CommentID = commentId - r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText) - } - - // parse reason - if r.ReportType > 0 { - r.Reason = &schema.ReasonItem{ - ReasonType: r.ReportType, - } - err = rs.configRepo.GetConfigById(r.ReportType, r.Reason) - } - if r.FlaggedType > 0 { - r.FlaggedReason = &schema.ReasonItem{ - ReasonType: r.FlaggedType, - } - _ = rs.configRepo.GetConfigById(r.FlaggedType, r.FlaggedReason) - } - - res[i] = r - } - resp = &res -} - -func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} diff --git a/internal/service/report_common/report_common.go b/internal/service/report_common/report_common.go index 5b5f3827b..64fcb838a 100644 --- a/internal/service/report_common/report_common.go +++ b/internal/service/report_common/report_common.go @@ -1,15 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package report_common import ( "context" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" ) // ReportRepo report repository type ReportRepo interface { AddReport(ctx context.Context, report *entity.Report) (err error) - GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) - GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) - UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) + GetReportListPage(ctx context.Context, query *schema.GetReportListPageDTO) ( + reports []*entity.Report, total int64, err error) + GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) + UpdateStatus(ctx context.Context, id string, status int) (err error) + GetReportCount(ctx context.Context) (count int64, err error) } diff --git a/internal/service/report_handle/report_handle.go b/internal/service/report_handle/report_handle.go new file mode 100644 index 000000000..27cffb111 --- /dev/null +++ b/internal/service/report_handle/report_handle.go @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package report_handle + +import ( + "context" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/comment" + "github.com/apache/answer/internal/service/content" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/obj" +) + +type ReportHandle struct { + questionService *content.QuestionService + answerService *content.AnswerService + commentService *comment.CommentService +} + +func NewReportHandle( + questionService *content.QuestionService, + answerService *content.AnswerService, + commentService *comment.CommentService, +) *ReportHandle { + return &ReportHandle{ + questionService: questionService, + answerService: answerService, + commentService: commentService, + } +} + +// UpdateReportedObject this handle object status +func (rh *ReportHandle) UpdateReportedObject(ctx context.Context, + report *entity.Report, req *schema.ReviewReportReq) (err error) { + objectKey, err := obj.GetObjectTypeStrByObjectID(report.ObjectID) + if err != nil { + return err + } + switch objectKey { + case constant.QuestionObjectType: + err = rh.updateReportedQuestionReport(ctx, report, req) + case constant.AnswerObjectType: + err = rh.updateReportedAnswerReport(ctx, report, req) + case constant.CommentObjectType: + err = rh.updateReportedCommentReport(ctx, report, req) + } + return +} + +func (rh *ReportHandle) updateReportedQuestionReport(ctx context.Context, + report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationUnlistPost: + err = rh.questionService.OperationQuestion(ctx, &schema.OperationQuestionReq{ + ID: report.ObjectID, Operation: schema.QuestionOperationHide, UserID: req.UserID}) + case constant.ReportOperationDeletePost: + err = rh.questionService.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ + ID: report.ObjectID, UserID: req.UserID, IsAdmin: true}) + case constant.ReportOperationClosePost: + err = rh.questionService.CloseQuestion(ctx, &schema.CloseQuestionReq{ + ID: report.ObjectID, + CloseType: req.CloseType, + CloseMsg: req.CloseMsg, + UserID: req.UserID, + }) + case constant.ReportOperationEditPost: + _, err = rh.questionService.UpdateQuestion(ctx, &schema.QuestionUpdate{ + ID: report.ObjectID, + Title: req.Title, + Content: req.Content, + HTML: converter.Markdown2HTML(req.Content), + Tags: req.Tags, + UserID: req.UserID, + NoNeedReview: true, + }) + } + return +} + +func (rh *ReportHandle) updateReportedAnswerReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationDeletePost: + err = rh.answerService.RemoveAnswer(ctx, &schema.RemoveAnswerReq{ + ID: report.ObjectID, UserID: req.UserID}) + case constant.ReportOperationEditPost: + _, err = rh.answerService.Update(ctx, &schema.AnswerUpdateReq{ + ID: report.ObjectID, + Title: req.Title, + Content: req.Content, + HTML: converter.Markdown2HTML(req.Content), + UserID: req.UserID, + NoNeedReview: true, + }) + } + return nil +} + +func (rh *ReportHandle) updateReportedCommentReport(ctx context.Context, report *entity.Report, req *schema.ReviewReportReq) (err error) { + switch req.OperationType { + case constant.ReportOperationDeletePost: + err = rh.commentService.RemoveComment(ctx, &schema.RemoveCommentReq{ + CommentID: report.ObjectID, UserID: req.UserID}) + case constant.ReportOperationEditPost: + _, err = rh.commentService.UpdateComment(ctx, &schema.UpdateCommentReq{ + CommentID: report.ObjectID, + OriginalText: req.Content, + ParsedText: converter.Markdown2HTML(req.Content), + UserID: req.UserID, + }) + } + return nil +} diff --git a/internal/service/report_handle_backyard/report_handle.go b/internal/service/report_handle_backyard/report_handle.go deleted file mode 100644 index 358c2a4c7..000000000 --- a/internal/service/report_handle_backyard/report_handle.go +++ /dev/null @@ -1,86 +0,0 @@ -package report_handle_backyard - -import ( - "context" - - "github.com/answerdev/answer/internal/service/config" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/comment" - "github.com/answerdev/answer/internal/service/notice_queue" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/pkg/obj" -) - -type ReportHandle struct { - questionCommon *questioncommon.QuestionCommon - commentRepo comment.CommentRepo - configRepo config.ConfigRepo -} - -func NewReportHandle( - questionCommon *questioncommon.QuestionCommon, - commentRepo comment.CommentRepo, - configRepo config.ConfigRepo) *ReportHandle { - return &ReportHandle{ - questionCommon: questionCommon, - commentRepo: commentRepo, - configRepo: configRepo, - } -} - -// HandleObject this handle object status -func (rh *ReportHandle) HandleObject(ctx context.Context, reported entity.Report, req schema.ReportHandleReq) (err error) { - var ( - objectID = reported.ObjectID - reportedUserID = reported.ReportedUserID - objectKey string - reasonDelete, _ = rh.configRepo.GetConfigType("reason.needs_delete") - reasonClose, _ = rh.configRepo.GetConfigType("reason.needs_close") - ) - - objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) - if err != nil { - return err - } - switch objectKey { - case "question": - switch req.FlaggedType { - case reasonDelete: - err = rh.questionCommon.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ID: objectID}) - case reasonClose: - err = rh.questionCommon.CloseQuestion(ctx, &schema.CloseQuestionReq{ - ID: objectID, - CloseType: req.FlaggedType, - CloseMsg: req.FlaggedContent, - }) - } - case "answer": - switch req.FlaggedType { - case reasonDelete: - err = rh.questionCommon.RemoveAnswer(ctx, objectID) - } - case "comment": - switch req.FlaggedType { - case reasonDelete: - err = rh.commentRepo.RemoveComment(ctx, objectID) - rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted) - } - } - return -} - -// sendNotification send rank triggered notification -func (rh *ReportHandle) sendNotification(ctx context.Context, reportedUserID, objectID, notificationAction string) { - msg := &schema.NotificationMsg{ - TriggerUserID: reportedUserID, - ReceiverUserID: reportedUserID, - Type: schema.NotificationTypeInbox, - ObjectID: objectID, - ObjectType: constant.ReportObjectType, - NotificationAction: notificationAction, - } - notice_queue.AddNotification(msg) -} diff --git a/internal/service/review/review_service.go b/internal/service/review/review_service.go new file mode 100644 index 000000000..7f934f236 --- /dev/null +++ b/internal/service/review/review_service.go @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package review + +import ( + "context" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/notice_queue" + "github.com/apache/answer/internal/service/object_info" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// ReviewRepo review repository +type ReviewRepo interface { + AddReview(ctx context.Context, review *entity.Review) (err error) + UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) + GetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error) + GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) + GetReviewCount(ctx context.Context, status int) (count int64, err error) + GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error) +} + +// ReviewService user service +type ReviewService struct { + reviewRepo ReviewRepo + objectInfoService *object_info.ObjService + userCommon *usercommon.UserCommon + userRepo usercommon.UserRepo + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + userRoleService *role.UserRoleRelService + tagCommon *tagcommon.TagCommonService + questionCommon *questioncommon.QuestionCommon + externalNotificationQueueService notice_queue.ExternalNotificationQueueService + notificationQueueService notice_queue.NotificationQueueService + siteInfoService siteinfo_common.SiteInfoCommonService +} + +// NewReviewService new review service +func NewReviewService( + reviewRepo ReviewRepo, + objectInfoService *object_info.ObjService, + userCommon *usercommon.UserCommon, + userRepo usercommon.UserRepo, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + userRoleService *role.UserRoleRelService, + externalNotificationQueueService notice_queue.ExternalNotificationQueueService, + tagCommon *tagcommon.TagCommonService, + questionCommon *questioncommon.QuestionCommon, + notificationQueueService notice_queue.NotificationQueueService, + siteInfoService siteinfo_common.SiteInfoCommonService, +) *ReviewService { + return &ReviewService{ + reviewRepo: reviewRepo, + objectInfoService: objectInfoService, + userCommon: userCommon, + userRepo: userRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + userRoleService: userRoleService, + externalNotificationQueueService: externalNotificationQueueService, + tagCommon: tagCommon, + questionCommon: questionCommon, + notificationQueueService: notificationQueueService, + siteInfoService: siteInfoService, + } +} + +// AddQuestionReview add review for question if needed +func (cs *ReviewService) AddQuestionReview(ctx context.Context, + question *entity.Question, tags []*schema.TagItem, ip, ua string) (questionStatus int) { + reviewContent := &plugin.ReviewContent{ + ObjectType: constant.QuestionObjectType, + Title: question.Title, + Content: question.ParsedText, + IP: ip, + UserAgent: ua, + } + for _, tag := range tags { + reviewContent.Tags = append(reviewContent.Tags, tag.SlugName) + } + reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, question.UserID) + reviewStatus := cs.callPluginToReview(ctx, question.UserID, question.ID, reviewContent) + switch reviewStatus { + case plugin.ReviewStatusApproved: + questionStatus = entity.QuestionStatusAvailable + case plugin.ReviewStatusNeedReview: + questionStatus = entity.QuestionStatusPending + case plugin.ReviewStatusDeleteDirectly: + questionStatus = entity.QuestionStatusDeleted + default: + questionStatus = entity.QuestionStatusAvailable + } + return questionStatus +} + +// AddAnswerReview add review for answer if needed +func (cs *ReviewService) AddAnswerReview(ctx context.Context, + answer *entity.Answer, ip, ua string) (answerStatus int) { + reviewContent := &plugin.ReviewContent{ + ObjectType: constant.AnswerObjectType, + Content: answer.ParsedText, + IP: ip, + UserAgent: ua, + } + reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, answer.UserID) + reviewStatus := cs.callPluginToReview(ctx, answer.UserID, answer.ID, reviewContent) + switch reviewStatus { + case plugin.ReviewStatusApproved: + answerStatus = entity.AnswerStatusAvailable + case plugin.ReviewStatusNeedReview: + answerStatus = entity.AnswerStatusPending + case plugin.ReviewStatusDeleteDirectly: + answerStatus = entity.AnswerStatusDeleted + default: + answerStatus = entity.AnswerStatusAvailable + } + return answerStatus +} + +// get review content author info +func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) { + user, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID) + if err != nil { + log.Errorf("get user info failed, err: %v", err) + return + } + if !exist { + log.Errorf("user not found by id: %s", userID) + return + } + author.Rank = user.Rank + author.ApprovedQuestionAmount, _ = cs.questionRepo.GetUserQuestionCount(ctx, userID, 0) + author.ApprovedAnswerAmount, _ = cs.answerRepo.GetCountByUserID(ctx, userID) + author.Role, _ = cs.userRoleService.GetUserRole(ctx, userID) + return +} + +// call plugin to review +func (cs *ReviewService) callPluginToReview(ctx context.Context, userID, objectID string, + reviewContent *plugin.ReviewContent) (reviewStatus plugin.ReviewStatus) { + // As default, no need review + reviewStatus = plugin.ReviewStatusApproved + objectID = uid.DeShortID(objectID) + + r := &entity.Review{ + UserID: userID, + ObjectID: objectID, + ObjectType: constant.ObjectTypeStrMapping[reviewContent.ObjectType], + ReviewerUserID: "0", + Status: entity.ReviewStatusPending, + } + if siteInterface, _ := cs.siteInfoService.GetSiteInterface(ctx); siteInterface != nil { + reviewContent.Language = siteInterface.Language + } + + _ = plugin.CallReviewer(func(reviewer plugin.Reviewer) error { + // If one of the reviewer plugin return false, then the review is not approved + if reviewStatus != plugin.ReviewStatusApproved { + return nil + } + if result := reviewer.Review(reviewContent); !result.Approved { + reviewStatus = result.ReviewStatus + r.Reason = result.Reason + r.Submitter = reviewer.Info().SlugName + } + return nil + }) + + if reviewStatus == plugin.ReviewStatusNeedReview { + if err := cs.reviewRepo.AddReview(ctx, r); err != nil { + log.Errorf("add review failed, err: %v", err) + } + } + return reviewStatus +} + +// UpdateReview update review +func (cs *ReviewService) UpdateReview(ctx context.Context, req *schema.UpdateReviewReq) (err error) { + review, exist, err := cs.reviewRepo.GetReview(ctx, req.ReviewID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if review.Status != entity.ReviewStatusPending { + return nil + } + + if err = cs.updateObjectStatus(ctx, review, req.IsApprove()); err != nil { + return err + } + + if req.IsApprove() { + err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusApproved) + } else { + err = cs.reviewRepo.UpdateReviewStatus(ctx, req.ReviewID, req.UserID, entity.ReviewStatusRejected) + } + return +} + +// update object status +func (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.Review, isApprove bool) (err error) { + objectType := constant.ObjectTypeNumberMapping[review.ObjectType] + switch objectType { + case constant.QuestionObjectType: + questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, review.ObjectID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + questionInfo.Status = entity.QuestionStatusAvailable + } else { + questionInfo.Status = entity.QuestionStatusDeleted + } + if err := cs.questionRepo.UpdateQuestionStatus(ctx, questionInfo.ID, questionInfo.Status); err != nil { + return err + } + if isApprove { + tags, err := cs.tagCommon.GetObjectEntityTag(ctx, questionInfo.ID) + if err != nil { + log.Errorf("get question tags failed, err: %v", err) + } + cs.externalNotificationQueueService.Send(ctx, + schema.CreateNewQuestionNotificationMsg(questionInfo.ID, questionInfo.Title, questionInfo.UserID, tags)) + } + userQuestionCount, err := cs.questionRepo.GetUserQuestionCount(ctx, questionInfo.UserID, 0) + if err != nil { + log.Errorf("get user question count failed, err: %v", err) + } else { + err = cs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount) + if err != nil { + log.Errorf("update user question count failed, err: %v", err) + } + } + case constant.AnswerObjectType: + answerInfo, exist, err := cs.answerRepo.GetAnswer(ctx, review.ObjectID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + answerInfo.Status = entity.AnswerStatusAvailable + } else { + answerInfo.Status = entity.AnswerStatusDeleted + } + if err := cs.answerRepo.UpdateAnswerStatus(ctx, answerInfo.ID, answerInfo.Status); err != nil { + return err + } + questionInfo, exist, err := cs.questionRepo.GetQuestion(ctx, answerInfo.QuestionID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.ObjectNotFound) + } + if isApprove { + cs.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, answerInfo.ID, + answerInfo.UserID, questionInfo.Title, answerInfo.OriginalText) + } + if err := cs.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID); err != nil { + log.Errorf("update question answer count failed, err: %v", err) + } + if err := cs.questionCommon.UpdateLastAnswer(ctx, answerInfo.QuestionID, uid.DeShortID(answerInfo.ID)); err != nil { + log.Errorf("update question last answer failed, err: %v", err) + } + userAnswerCount, err := cs.answerRepo.GetCountByUserID(ctx, answerInfo.UserID) + if err != nil { + log.Errorf("get user answer count failed, err: %v", err) + } else { + err = cs.userCommon.UpdateAnswerCount(ctx, answerInfo.UserID, int(userAnswerCount)) + if err != nil { + log.Errorf("update user answer count failed, err: %v", err) + } + } + } + return +} + +func (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context, + questionUserID, questionID, answerID, answerUserID, questionTitle, answerSummary string) { + // If the question is answered by me, there is no notification for myself. + if questionUserID == answerUserID { + return + } + msg := &schema.NotificationMsg{ + TriggerUserID: answerUserID, + ReceiverUserID: questionUserID, + Type: schema.NotificationTypeInbox, + ObjectID: answerID, + } + msg.ObjectType = constant.AnswerObjectType + msg.NotificationAction = constant.NotificationAnswerTheQuestion + cs.notificationQueueService.Send(ctx, msg) + + receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) + if err != nil { + log.Error(err) + return + } + if !exist { + log.Warnf("user %s not found", questionUserID) + return + } + + externalNotificationMsg := &schema.ExternalNotificationMsg{ + ReceiverUserID: receiverUserInfo.ID, + ReceiverEmail: receiverUserInfo.EMail, + ReceiverLang: receiverUserInfo.Language, + } + rawData := &schema.NewAnswerTemplateRawData{ + QuestionTitle: questionTitle, + QuestionID: questionID, + AnswerID: answerID, + AnswerSummary: answerSummary, + UnsubscribeCode: token.GenerateToken(), + } + answerUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, answerUserID) + if answerUser != nil { + rawData.AnswerUserDisplayName = answerUser.DisplayName + } + externalNotificationMsg.NewAnswerTemplateRawData = rawData + cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg) +} + +// GetReviewPendingCount get review pending count +func (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) { + return cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending) +} + +// GetUnreviewedPostPage get review page +func (cs *ReviewService) GetUnreviewedPostPage(ctx context.Context, req *schema.GetUnreviewedPostPageReq) ( + pageModel *pager.PageModel, err error) { + if !req.IsAdmin { + return pager.NewPageModel(0, make([]*schema.GetUnreviewedPostPageResp, 0)), nil + } + cond := &entity.Review{ + ObjectID: req.ObjectID, + Status: entity.ReviewStatusPending, + } + reviewList, total, err := cs.reviewRepo.GetReviewPage(ctx, req.Page, 1, cond) + if err != nil { + return + } + + resp := make([]*schema.GetUnreviewedPostPageResp, 0) + for _, review := range reviewList { + info, err := cs.objectInfoService.GetUnreviewedRevisionInfo(ctx, review.ObjectID) + if err != nil { + log.Errorf("GetUnreviewedRevisionInfo failed, err: %v", err) + continue + } + + r := &schema.GetUnreviewedPostPageResp{ + ReviewID: review.ID, + CreatedAt: info.CreatedAt, + ObjectID: info.ObjectID, + QuestionID: info.QuestionID, + AnswerID: info.AnswerID, + CommentID: info.CommentID, + ObjectType: info.ObjectType, + Title: info.Title, + UrlTitle: htmltext.UrlTitle(info.Title), + OriginalText: info.Content, + ParsedText: info.Html, + Tags: info.Tags, + ObjectStatus: info.Status, + ObjectShowStatus: info.ShowStatus, + SubmitAt: review.CreatedAt.Unix(), + SubmitterDisplayName: req.ReviewerMapping[review.Submitter], + Reason: review.Reason, + } + + // get user info + userInfo, exists, e := cs.userCommon.GetUserBasicInfoByID(ctx, info.ObjectCreatorUserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", info.ObjectCreatorUserID, e) + } + if exists { + _ = copier.Copy(&r.AuthorUserInfo, userInfo) + } + resp = append(resp, r) + } + return pager.NewPageModel(total, resp), nil +} diff --git a/internal/service/revision/revision.go b/internal/service/revision/revision.go index 333d83797..33ce285e0 100644 --- a/internal/service/revision/revision.go +++ b/internal/service/revision/revision.go @@ -1,16 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package revision import ( "context" - "github.com/answerdev/answer/internal/entity" + + "github.com/apache/answer/internal/entity" "xorm.io/xorm" ) // RevisionRepo revision repository type RevisionRepo interface { AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) - GetRevision(ctx context.Context, id string) (revision *entity.Revision, exist bool, err error) + GetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error) GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) + GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) + ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) + GetUnreviewedRevisionPage(ctx context.Context, page, pageSize int, objectTypes []int) ([]*entity.Revision, int64, error) + CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) + UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) } diff --git a/internal/service/revision_common/revision_service.go b/internal/service/revision_common/revision_service.go index cee537105..7275916e1 100644 --- a/internal/service/revision_common/revision_service.go +++ b/internal/service/revision_common/revision_service.go @@ -1,13 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package revision_common import ( "context" - "github.com/answerdev/answer/internal/service/revision" - usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/revision" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/uid" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" ) @@ -17,24 +40,56 @@ type RevisionService struct { userRepo usercommon.UserRepo } -func NewRevisionService(revisionRepo revision.RevisionRepo, userRepo usercommon.UserRepo) *RevisionService { +func NewRevisionService(revisionRepo revision.RevisionRepo, + userRepo usercommon.UserRepo, +) *RevisionService { return &RevisionService{ revisionRepo: revisionRepo, userRepo: userRepo, } } +func (rs *RevisionService) GetUnreviewedRevisionCount(ctx context.Context, req *schema.RevisionSearch) (count int64, err error) { + if len(req.GetCanReviewObjectTypes()) == 0 { + return 0, nil + } + return rs.revisionRepo.CountUnreviewedRevision(ctx, req.GetCanReviewObjectTypes()) +} + // AddRevision add revision // // autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated, // if not need auto update object.revision_id, it must be false. // example: user can edit the object, but need audit, the revision_id will be updated when admin approved -func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) (err error) { +func (rs *RevisionService) AddRevision(ctx context.Context, req *schema.AddRevisionDTO, autoUpdateRevisionID bool) ( + revisionID string, err error) { + req.ObjectID = uid.DeShortID(req.ObjectID) rev := &entity.Revision{} _ = copier.Copy(rev, req) err = rs.revisionRepo.AddRevision(ctx, rev, autoUpdateRevisionID) if err != nil { - return err + return "", err } - return nil + return rev.ID, nil +} + +// GetRevision get revision +func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) ( + revision *entity.Revision, err error) { + revisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID) + if err != nil { + log.Error(err) + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.ObjectNotFound) + } + return revisionInfo, nil +} + +// ExistUnreviewedByObjectID +func (rs *RevisionService) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) { + objectID = uid.DeShortID(objectID) + revision, exist, err = rs.revisionRepo.ExistUnreviewedByObjectID(ctx, objectID) + return revision, exist, err } diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go deleted file mode 100644 index 1e9b21d60..000000000 --- a/internal/service/revision_service.go +++ /dev/null @@ -1,152 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/revision" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" -) - -// RevisionService user service -type RevisionService struct { - revisionRepo revision.RevisionRepo - userCommon *usercommon.UserCommon - questionCommon *questioncommon.QuestionCommon - answerService *AnswerService -} - -func NewRevisionService( - revisionRepo revision.RevisionRepo, - userCommon *usercommon.UserCommon, - questionCommon *questioncommon.QuestionCommon, - answerService *AnswerService) *RevisionService { - return &RevisionService{ - revisionRepo: revisionRepo, - userCommon: userCommon, - questionCommon: questionCommon, - answerService: answerService, - } -} - -// GetRevision get revision one -func (rs *RevisionService) GetRevision(ctx context.Context, id string) (resp schema.GetRevisionResp, err error) { - var ( - rev *entity.Revision - exists bool - ) - - resp = schema.GetRevisionResp{} - - rev, exists, err = rs.revisionRepo.GetRevision(ctx, id) - if err != nil { - return - } - - if !exists { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - - _ = copier.Copy(&resp, rev) - rs.parseItem(ctx, &resp) - - return -} - -// GetRevisionList get revision list all -func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { - var ( - rev entity.Revision - revs []entity.Revision - ) - - resp = []schema.GetRevisionResp{} - _ = copier.Copy(&rev, req) - - revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev) - if err != nil { - return - } - - for _, r := range revs { - var ( - uinfo schema.UserBasicInfo - item schema.GetRevisionResp - ) - - _ = copier.Copy(&item, r) - rs.parseItem(ctx, &item) - - // get user info - userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID) - if e != nil { - return nil, e - } - if exists { - err = copier.Copy(&uinfo, userInfo) - item.UserInfo = uinfo - } - resp = append(resp, item) - } - return -} - -func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisionResp) { - var ( - err error - question entity.Question - questionInfo *schema.QuestionInfo - answer entity.Answer - answerInfo *schema.AnswerInfo - tag entity.Tag - tagInfo *schema.GetTagResp - ) - - switch item.ObjectType { - case constant.ObjectTypeStrMapping["question"]: - err = json.Unmarshal([]byte(item.Content), &question) - if err != nil { - break - } - questionInfo = rs.questionCommon.ShowFormat(ctx, &question) - item.ContentParsed = questionInfo - case constant.ObjectTypeStrMapping["answer"]: - err = json.Unmarshal([]byte(item.Content), &answer) - if err != nil { - break - } - answerInfo = rs.answerService.ShowFormat(ctx, &answer) - item.ContentParsed = answerInfo - case constant.ObjectTypeStrMapping["tag"]: - err = json.Unmarshal([]byte(item.Content), &tag) - if err != nil { - break - } - tagInfo = &schema.GetTagResp{ - TagID: tag.ID, - CreatedAt: tag.CreatedAt.Unix(), - UpdatedAt: tag.UpdatedAt.Unix(), - SlugName: tag.SlugName, - DisplayName: tag.DisplayName, - OriginalText: tag.OriginalText, - ParsedText: tag.ParsedText, - FollowCount: tag.FollowCount, - QuestionCount: tag.QuestionCount, - } - tagInfo.GetExcerpt() - item.ContentParsed = tagInfo - } - - if err != nil { - item.ContentParsed = item.Content - } - item.CreatedAtParsed = item.CreatedAt.Unix() -} diff --git a/internal/service/role/power_service.go b/internal/service/role/power_service.go new file mode 100644 index 000000000..f8289963f --- /dev/null +++ b/internal/service/role/power_service.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/entity" +) + +// PowerRepo power repository +type PowerRepo interface { + GetPowerList(ctx context.Context, power *entity.Power) (powers []*entity.Power, err error) +} diff --git a/internal/service/role/role_power_rel_service.go b/internal/service/role/role_power_rel_service.go new file mode 100644 index 000000000..18fbbdea1 --- /dev/null +++ b/internal/service/role/role_power_rel_service.go @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" +) + +// RolePowerRelRepo rolePowerRel repository +type RolePowerRelRepo interface { + GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) +} + +// RolePowerRelService user service +type RolePowerRelService struct { + rolePowerRelRepo RolePowerRelRepo + userRoleRelService *UserRoleRelService +} + +// NewRolePowerRelService new role power rel service +func NewRolePowerRelService(rolePowerRelRepo RolePowerRelRepo, + userRoleRelService *UserRoleRelService) *RolePowerRelService { + return &RolePowerRelService{ + rolePowerRelRepo: rolePowerRelRepo, + userRoleRelService: userRoleRelService, + } +} + +// GetRolePowerList get role power list +func (rs *RolePowerRelService) GetRolePowerList(ctx context.Context, roleID int) (powers []string, err error) { + return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID) +} + +// GetUserPowerList get list all +func (rs *RolePowerRelService) GetUserPowerList(ctx context.Context, userID string) (powers []string, err error) { + roleID, err := rs.userRoleRelService.GetUserRole(ctx, userID) + if err != nil { + return nil, err + } + return rs.rolePowerRelRepo.GetRolePowerTypeList(ctx, roleID) +} diff --git a/internal/service/role/role_service.go b/internal/service/role/role_service.go new file mode 100644 index 000000000..96e8f6d75 --- /dev/null +++ b/internal/service/role/role_service.go @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/jinzhu/copier" +) + +const ( + // Since there is currently no need to edit roles to add roles and other operations, + // the current role information is translated directly. + // Later on, when the relevant ability is available, it can be adjusted by the user himself. + + RoleUserID = 1 + RoleAdminID = 2 + RoleModeratorID = 3 + + roleUserName = "User" + roleAdminName = "Admin" + roleModeratorName = "Moderator" + + trRoleNameUser = "role.name.user" + trRoleNameAdmin = "role.name.admin" + trRoleNameModerator = "role.name.moderator" + + trRoleDescriptionUser = "role.description.user" + trRoleDescriptionAdmin = "role.description.admin" + trRoleDescriptionModerator = "role.description.moderator" +) + +// RoleRepo role repository +type RoleRepo interface { + GetRoleAllList(ctx context.Context) (roles []*entity.Role, err error) + GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) +} + +// RoleService user service +type RoleService struct { + roleRepo RoleRepo +} + +func NewRoleService(roleRepo RoleRepo) *RoleService { + return &RoleService{ + roleRepo: roleRepo, + } +} + +// GetRoleList get role list all +func (rs *RoleService) GetRoleList(ctx context.Context) (resp []*schema.GetRoleResp, err error) { + roles, err := rs.roleRepo.GetRoleAllList(ctx) + if err != nil { + return + } + + for _, role := range roles { + rs.translateRole(ctx, role) + } + + resp = []*schema.GetRoleResp{} + _ = copier.Copy(&resp, roles) + return +} + +func (rs *RoleService) GetRoleMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) { + return rs.roleRepo.GetRoleAllMapping(ctx) +} + +func (rs *RoleService) translateRole(ctx context.Context, role *entity.Role) { + switch role.Name { + case roleUserName: + role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameUser) + role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionUser) + case roleAdminName: + role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameAdmin) + role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionAdmin) + case roleModeratorName: + role.Name = translator.Tr(handler.GetLangByCtx(ctx), trRoleNameModerator) + role.Description = translator.Tr(handler.GetLangByCtx(ctx), trRoleDescriptionModerator) + } +} diff --git a/internal/service/role/user_role_rel_service.go b/internal/service/role/user_role_rel_service.go new file mode 100644 index 000000000..d80863f45 --- /dev/null +++ b/internal/service/role/user_role_rel_service.go @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package role + +import ( + "context" + + "github.com/apache/answer/internal/entity" +) + +// UserRoleRelRepo userRoleRel repository +type UserRoleRelRepo interface { + SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) + GetUserRoleRelList(ctx context.Context, userIDs []string) (userRoleRelList []*entity.UserRoleRel, err error) + GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) ( + userRoleRelList []*entity.UserRoleRel, err error) + GetUserRoleRel(ctx context.Context, userID string) (rolePowerRel *entity.UserRoleRel, exist bool, err error) +} + +// UserRoleRelService user service +type UserRoleRelService struct { + userRoleRelRepo UserRoleRelRepo + roleService *RoleService +} + +// NewUserRoleRelService new user role rel service +func NewUserRoleRelService(userRoleRelRepo UserRoleRelRepo, roleService *RoleService) *UserRoleRelService { + return &UserRoleRelService{ + userRoleRelRepo: userRoleRelRepo, + roleService: roleService, + } +} + +// SaveUserRole save user role +func (us *UserRoleRelService) SaveUserRole(ctx context.Context, userID string, roleID int) (err error) { + return us.userRoleRelRepo.SaveUserRoleRel(ctx, userID, roleID) +} + +// GetUserRoleMapping get user role mapping +func (us *UserRoleRelService) GetUserRoleMapping(ctx context.Context, userIDs []string) ( + userRoleMapping map[string]*entity.Role, err error) { + userRoleMapping = make(map[string]*entity.Role, 0) + roleMapping, err := us.roleService.GetRoleMapping(ctx) + if err != nil { + return userRoleMapping, err + } + if len(roleMapping) == 0 { + return userRoleMapping, nil + } + + relMapping, err := us.GetUserRoleRelMapping(ctx, userIDs) + if err != nil { + return userRoleMapping, err + } + + // default role is user + defaultRole := roleMapping[1] + for _, userID := range userIDs { + roleID, ok := relMapping[userID] + if !ok { + userRoleMapping[userID] = defaultRole + continue + } + userRoleMapping[userID] = roleMapping[roleID] + if userRoleMapping[userID] == nil { + userRoleMapping[userID] = defaultRole + } + } + return userRoleMapping, nil +} + +// GetUserRoleRelMapping get user role rel mapping +func (us *UserRoleRelService) GetUserRoleRelMapping(ctx context.Context, userIDs []string) ( + userRoleRelMapping map[string]int, err error) { + userRoleRelMapping = make(map[string]int, 0) + + relList, err := us.userRoleRelRepo.GetUserRoleRelList(ctx, userIDs) + if err != nil { + return userRoleRelMapping, err + } + + for _, rel := range relList { + userRoleRelMapping[rel.UserID] = rel.RoleID + } + return userRoleRelMapping, nil +} + +// GetUserRole get user role +func (us *UserRoleRelService) GetUserRole(ctx context.Context, userID string) (roleID int, err error) { + rolePowerRel, exist, err := us.userRoleRelRepo.GetUserRoleRel(ctx, userID) + if err != nil { + return 0, err + } + if !exist { + // set default role + return 1, nil + } + return rolePowerRel.RoleID, nil +} + +// GetUserByRoleID get user by role id +func (us *UserRoleRelService) GetUserByRoleID(ctx context.Context, roleIDs []int) (rel []*entity.UserRoleRel, err error) { + rolePowerRels, err := us.userRoleRelRepo.GetUserRoleRelListByRoleID(ctx, roleIDs) + if err != nil { + return nil, err + } + return rolePowerRels, nil +} diff --git a/internal/service/search/accepted_answer.go b/internal/service/search/accepted_answer.go deleted file mode 100644 index fafe2a842..000000000 --- a/internal/service/search/accepted_answer.go +++ /dev/null @@ -1,55 +0,0 @@ -package search - -import ( - "context" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" -) - -type AcceptedAnswerSearch struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch { - return &AcceptedAnswerSearch{ - repo: repo, - } -} - -func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q, - w, - p string - ) - - q = dto.Query - w = dto.Query - p = `isaccepted:yes` - - if strings.Index(q, p) == 0 { - ok = true - w = strings.TrimPrefix(q, p) - } - - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - - words := strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order) -} diff --git a/internal/service/search/answer.go b/internal/service/search/answer.go deleted file mode 100644 index d3f6fb092..000000000 --- a/internal/service/search/answer.go +++ /dev/null @@ -1,54 +0,0 @@ -package search - -import ( - "context" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "strings" -) - -type AnswerSearch struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch { - return &AnswerSearch{ - repo: repo, - } -} - -func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q, - w, - p string - ) - - q = dto.Query - w = dto.Query - p = `is:answer` - - if strings.Index(q, p) == 0 { - ok = true - w = strings.TrimPrefix(q, p) - } - - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - - words := strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order) -} diff --git a/internal/service/search/answers.go b/internal/service/search/answers.go deleted file mode 100644 index 6147f3c15..000000000 --- a/internal/service/search/answers.go +++ /dev/null @@ -1,65 +0,0 @@ -package search - -import ( - "context" - "regexp" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "github.com/answerdev/answer/pkg/converter" -) - -type AnswersSearch struct { - repo search_common.SearchRepo - exp int - w string - page int - size int - order string -} - -func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch { - return &AnswersSearch{ - repo: repo, - } -} - -func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q, - w, - p, - exp string - ) - - q = dto.Query - w = dto.Query - p = `(?m)^answers:([0-9]+)` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - - s.exp = converter.StringToInt(exp) - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} - -func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - - words := strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order) -} diff --git a/internal/service/search/author.go b/internal/service/search/author.go deleted file mode 100644 index d0a293b5d..000000000 --- a/internal/service/search/author.go +++ /dev/null @@ -1,90 +0,0 @@ -package search - -import ( - "context" - "regexp" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - usercommon "github.com/answerdev/answer/internal/service/user_common" -) - -type AuthorSearch struct { - repo search_common.SearchRepo - userCommon *usercommon.UserCommon - exp string - w string - page int - size int - order string -} - -func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch { - return &AuthorSearch{ - repo: repo, - userCommon: userCommon, - } -} - -// Parse -// example: "user:12345" -> {exp="" w="12345"} -func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - exp, - q, - w, - p, - me, - name string - ) - exp = "" - q = dto.Query - w = q - p = `(?m)^user:([a-z0-9._-]+)` - me = "user:me" - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - name = res[1] - user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name) - if err == nil && has { - exp = user.ID - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - } else if strings.Index(q, me) == 0 { - exp = dto.UserID - w = strings.TrimPrefix(q, me) - ok = true - } - - w = strings.TrimSpace(w) - s.exp = exp - s.w = w - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} - -func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - ) - - if len(s.exp) == 0 { - return - } - - words = strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order) - - return -} diff --git a/internal/service/search/in_question.go b/internal/service/search/in_question.go deleted file mode 100644 index 517c3d067..000000000 --- a/internal/service/search/in_question.go +++ /dev/null @@ -1,65 +0,0 @@ -package search - -import ( - "context" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "regexp" - "strings" -) - -type InQuestionSearch struct { - repo search_common.SearchRepo - w string - exp string - page int - size int - order string -} - -func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch { - return &InQuestionSearch{ - repo: repo, - } -} - -func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - w, - q, - p, - exp string - ) - - q = dto.Query - w = dto.Query - p = `(?m)^inquestion:([0-9]+)` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - - s.exp = exp - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - ) - - words = strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order) -} diff --git a/internal/service/search/not_accepted_question.go b/internal/service/search/not_accepted_question.go deleted file mode 100644 index e2c346367..000000000 --- a/internal/service/search/not_accepted_question.go +++ /dev/null @@ -1,58 +0,0 @@ -package search - -import ( - "context" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" -) - -type NotAcceptedQuestion struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewNotAcceptedQuestion(repo search_common.SearchRepo) *NotAcceptedQuestion { - return &NotAcceptedQuestion{ - repo: repo, - } -} - -func (s *NotAcceptedQuestion) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q, - w, - p string - ) - - q = dto.Query - w = dto.Query - p = `hasaccepted:no` - - if strings.Index(q, p) == 0 { - ok = true - w = strings.TrimPrefix(q, p) - } - - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - ) - - words = strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size, s.order) -} diff --git a/internal/service/search/object.go b/internal/service/search/object.go deleted file mode 100644 index c35564d42..000000000 --- a/internal/service/search/object.go +++ /dev/null @@ -1,47 +0,0 @@ -package search - -import ( - "context" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" -) - -type ObjectSearch struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewObjectSearch(repo search_common.SearchRepo) *ObjectSearch { - return &ObjectSearch{ - repo: repo, - } -} - -func (s *ObjectSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - w string - ) - w = strings.TrimSpace(dto.Query) - if len(w) > 0 { - ok = true - } - - s.w = w - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - - words := strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size, s.order) -} diff --git a/internal/service/search/question.go b/internal/service/search/question.go deleted file mode 100644 index 83e7713eb..000000000 --- a/internal/service/search/question.go +++ /dev/null @@ -1,55 +0,0 @@ -package search - -import ( - "context" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "strings" -) - -type QuestionSearch struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewQuestionSearch(repo search_common.SearchRepo) *QuestionSearch { - return &QuestionSearch{ - repo: repo, - } -} - -func (s *QuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q, - w, - p string - ) - - q = dto.Query - w = dto.Query - p = `is:question` - - if strings.Index(q, p) == 0 { - ok = true - w = strings.TrimPrefix(q, p) - } - - s.w = strings.TrimSpace(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} - -func (s *QuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - - words := strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size, s.order) -} diff --git a/internal/service/search/score.go b/internal/service/search/score.go deleted file mode 100644 index 71287ec7c..000000000 --- a/internal/service/search/score.go +++ /dev/null @@ -1,63 +0,0 @@ -package search - -import ( - "context" - "regexp" - "strings" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "github.com/answerdev/answer/pkg/converter" -) - -type ScoreSearch struct { - repo search_common.SearchRepo - exp int - w string - page int - size int - order string -} - -func NewScoreSearch(repo search_common.SearchRepo) *ScoreSearch { - return &ScoreSearch{ - repo: repo, - } -} - -func (s *ScoreSearch) Parse(dto *schema.SearchDTO) (ok bool) { - exp := "" - q := dto.Query - w := q - p := `(?m)^score:([0-9]+)` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(w) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - - w = strings.TrimSpace(w) - s.exp = converter.StringToInt(exp) - s.w = w - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} -func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - ) - - words = strings.Split(s.w, " ") - if len(words) > 3 { - words = words[:4] - } - - resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size, s.order) - return -} diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go deleted file mode 100644 index 73bd3a719..000000000 --- a/internal/service/search/tag.go +++ /dev/null @@ -1,99 +0,0 @@ -package search - -import ( - "context" - "regexp" - "strings" - - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/search_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" -) - -type TagSearch struct { - repo search_common.SearchRepo - tagRepo tagcommon.TagRepo - followCommon activity_common.FollowRepo - page int - size int - exp string - w string - userID string - Extra schema.GetTagPageResp - order string -} - -func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch { - return &TagSearch{ - repo: repo, - tagRepo: tagRepo, - followCommon: followCommon, - } -} - -// Parse -// example: "[tag]hello" -> {exp="tag" w="hello"} -func (ts *TagSearch) Parse(dto *schema.SearchDTO) (ok bool) { - exp := "" - w := dto.Query - q := w - p := `(?m)^\[([a-zA-Z0-9-\+\.#]+)\]` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - w = q[trimLen:] - ok = true - } - w = strings.TrimSpace(w) - ts.exp = exp - ts.w = w - ts.page = dto.Page - ts.size = dto.Size - ts.userID = dto.UserID - ts.order = dto.Order - return ok -} - -func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - var ( - words []string - tag *entity.Tag - exists, followed bool - ) - tag, exists, err = ts.tagRepo.GetTagBySlugName(ctx, ts.exp) - if err != nil { - return - } - - if ts.userID != "" { - followed, err = ts.followCommon.IsFollowed(ts.userID, tag.ID) - } - - ts.Extra = schema.GetTagPageResp{ - TagID: tag.ID, - SlugName: tag.SlugName, - DisplayName: tag.DisplayName, - OriginalText: tag.OriginalText, - ParsedText: tag.ParsedText, - QuestionCount: tag.QuestionCount, - IsFollower: followed, - } - ts.Extra.GetExcerpt() - - if !exists { - return - } - words = strings.Split(ts.w, " ") - if len(words) > 3 { - words = words[:4] - } - - resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size, ts.order) - - return -} diff --git a/internal/service/search/views.go b/internal/service/search/views.go deleted file mode 100644 index 49e4db6a2..000000000 --- a/internal/service/search/views.go +++ /dev/null @@ -1,47 +0,0 @@ -package search - -import ( - "context" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" - "regexp" - "strings" -) - -type ViewsSearch struct { - repo search_common.SearchRepo - exp string - q string - order string -} - -func NewViewsSearch(repo search_common.SearchRepo) *ViewsSearch { - return &ViewsSearch{ - repo: repo, - } -} - -func (s *ViewsSearch) Parse(dto *schema.SearchDTO) (ok bool) { - exp := "" - w := dto.Query - q := w - p := `(?m)^views:([0-9]+)` - - re := regexp.MustCompile(p) - res := re.FindStringSubmatch(q) - if len(res) == 2 { - exp = res[1] - trimLen := len(res[0]) - q = w[trimLen:] - ok = true - } - - q = strings.TrimSpace(q) - s.exp = exp - s.q = q - s.order = dto.Order - return -} -func (s *ViewsSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - return -} diff --git a/internal/service/search/within.go b/internal/service/search/within.go deleted file mode 100644 index 9461c23db..000000000 --- a/internal/service/search/within.go +++ /dev/null @@ -1,59 +0,0 @@ -package search - -import ( - "context" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/search_common" -) - -type WithinSearch struct { - repo search_common.SearchRepo - w string - page int - size int - order string -} - -func NewWithinSearch(repo search_common.SearchRepo) *WithinSearch { - return &WithinSearch{ - repo: repo, - } -} - -func (s *WithinSearch) Parse(dto *schema.SearchDTO) (ok bool) { - var ( - q string - w []rune - hasEnd bool - ) - - q = dto.Query - - if q[0:1] == `"` { - for _, v := range []rune(q) { - if len(w) == 0 && string(v) == `"` { - continue - } else if string(v) == `"` { - hasEnd = true - break - } else { - w = append(w, v) - } - } - } - - if hasEnd { - ok = true - } - - s.w = string(w) - s.page = dto.Page - s.size = dto.Size - s.order = dto.Order - return -} - -func (s *WithinSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) { - return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size, s.order) -} diff --git a/internal/service/search_common/search.go b/internal/service/search_common/search.go index 7f51cedaa..d26320c5d 100644 --- a/internal/service/search_common/search.go +++ b/internal/service/search_common/search.go @@ -1,12 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package search_common import ( "context" - "github.com/answerdev/answer/internal/schema" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/plugin" ) type SearchRepo interface { - SearchContents(ctx context.Context, words []string, tagID, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error) - SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) - SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) + SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) + SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) + SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) + ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) } diff --git a/internal/service/search_parser/search_parser.go b/internal/service/search_parser/search_parser.go new file mode 100644 index 000000000..e87efaf6f --- /dev/null +++ b/internal/service/search_parser/search_parser.go @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package search_parser + +import ( + "context" + "fmt" + "github.com/apache/answer/internal/base/constant" + "regexp" + "strings" + + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/tag_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/converter" +) + +type SearchParser struct { + tagCommonService *tag_common.TagCommonService + userCommon *usercommon.UserCommon +} + +func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser { + return &SearchParser{ + tagCommonService: tagCommonService, + userCommon: userCommon, + } +} + +// ParseStructure parse search structure, maybe match one of type all/questions/answers, +// but if match two type, it will return false +func (sp *SearchParser) ParseStructure(ctx context.Context, dto *schema.SearchDTO) (cond *schema.SearchCondition) { + cond = &schema.SearchCondition{} + var ( + query = dto.Query + limitWords = 5 + ) + + // match tags + cond.Tags = sp.parseTags(ctx, &query) + + // match all + cond.UserID = sp.parseUserID(ctx, &query, dto.UserID) + cond.VoteAmount = sp.parseVotes(&query) + cond.Words = sp.parseWithin(&query) + + // match questions + cond.NotAccepted = sp.parseNotAccepted(&query) + if cond.NotAccepted { + cond.TargetType = constant.QuestionObjectType + } + cond.Views = sp.parseViews(&query) + if cond.Views != -1 { + cond.TargetType = constant.QuestionObjectType + } + cond.AnswerAmount = sp.parseAnswers(&query) + if cond.AnswerAmount != -1 { + cond.TargetType = constant.QuestionObjectType + } + + // match answers + cond.Accepted = sp.parseAccepted(&query) + if cond.Accepted { + cond.TargetType = constant.AnswerObjectType + } + cond.QuestionID = sp.parseQuestionID(&query) + if cond.QuestionID != "" { + cond.TargetType = constant.AnswerObjectType + } + + if sp.parseIsQuestion(&query) { + cond.TargetType = constant.QuestionObjectType + } + if sp.parseIsAnswer(&query) { + cond.TargetType = constant.AnswerObjectType + } + + if len(strings.TrimSpace(query)) > 0 { + words := strings.Split(strings.TrimSpace(query), " ") + cond.Words = append(cond.Words, words...) + } + + // check limit words + if len(cond.Words) > limitWords { + cond.Words = cond.Words[:limitWords] + } + return +} + +// parseTags parse search tags, return tag ids array +func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][]string) { + var ( + // expire tag pattern + exprTag = `\[(.*?)\]` + q = *query + limit = 5 + ) + + re := regexp.MustCompile(exprTag) + res := re.FindAllStringSubmatch(q, -1) + if len(res) == 0 { + return + } + + tags = make([][]string, 0) + for _, item := range res { + tagGroup := make([]string, 0) + tag, exists, err := sp.tagCommonService.GetTagBySlugName(ctx, item[1]) + if err != nil || !exists { + continue + } + tagGroup = append(tagGroup, tag.ID) + if tag.MainTagID > 0 { + tagGroup = append(tagGroup, fmt.Sprintf("%d", tag.MainTagID)) + } + synIDs, err := sp.tagCommonService.GetTagIDsByMainTagID(ctx, tag.ID) + if err != nil || !exists { + continue + } + tagGroup = append(tagGroup, tag.ID) + tagGroup = append(tagGroup, synIDs...) + tagGroup = converter.UniqueArray(tagGroup) + tags = append(tags, tagGroup) + } + + // limit maximum 5 tags + if len(tags) > limit { + tags = tags[:limit] + } + + q = strings.TrimSpace(re.ReplaceAllString(q, "")) + *query = q + return +} + +// parseUserID return user id or current login user id +func (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) { + var ( + exprUsername = `user:(\S+)` + exprMe = "user:me" + q = *query + ) + + re := regexp.MustCompile(exprUsername) + res := re.FindStringSubmatch(q) + if strings.Contains(q, exprMe) { + userID = currentUserID + q = strings.ReplaceAll(q, exprMe, "") + } else if len(res) > 1 { + name := res[1] + user, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name) + if err == nil && has { + userID = user.ID + q = re.ReplaceAllString(q, "") + } + } + *query = strings.TrimSpace(q) + return +} + +// parseVotes return the votes of search query +func (sp *SearchParser) parseVotes(query *string) (votes int) { + var ( + expr = `score:(\d+)` + q = *query + ) + votes = -1 + + re := regexp.MustCompile(expr) + res := re.FindStringSubmatch(q) + if len(res) > 1 { + votes = converter.StringToInt(res[1]) + q = re.ReplaceAllString(q, "") + } + + *query = strings.TrimSpace(q) + return +} + +// parseWithin parse quotes within words like: "hello world" +func (sp *SearchParser) parseWithin(query *string) (words []string) { + var ( + q = *query + expr = `(?U)(".+")` + ) + re := regexp.MustCompile(expr) + matches := re.FindAllStringSubmatch(q, -1) + words = []string{} + for _, match := range matches { + if len(match[1]) == 0 { + continue + } + words = append(words, match[1]) + } + q = re.ReplaceAllString(q, "") + *query = strings.TrimSpace(q) + return +} + +// parseNotAccepted return the question has not accepted the answer +func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) { + var ( + q = *query + expr = `hasaccepted:no` + ) + + if strings.Contains(q, expr) { + q = strings.ReplaceAll(q, expr, "") + notAccepted = true + } + + *query = strings.TrimSpace(q) + return +} + +// parseIsQuestion check the result if only limit question or not +func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) { + var ( + q = *query + expr = `is:question` + ) + + if strings.Contains(q, expr) { + q = strings.ReplaceAll(q, expr, "") + isQuestion = true + } + + *query = strings.TrimSpace(q) + return +} + +// parseViews check search has views or not +func (sp *SearchParser) parseViews(query *string) (views int) { + var ( + q = *query + expr = `views:(\d+)` + ) + views = -1 + + re := regexp.MustCompile(expr) + res := re.FindStringSubmatch(q) + if len(res) > 1 { + views = converter.StringToInt(res[1]) + q = re.ReplaceAllString(q, "") + } + *query = strings.TrimSpace(q) + return +} + +// parseAnswers check whether specified answer count for question +func (sp *SearchParser) parseAnswers(query *string) (answers int) { + var ( + q = *query + expr = `answers:(\d+)` + ) + answers = -1 + + re := regexp.MustCompile(expr) + res := re.FindStringSubmatch(q) + if len(res) > 1 { + answers = converter.StringToInt(res[1]) + q = re.ReplaceAllString(q, "") + } + + *query = strings.TrimSpace(q) + return +} + +// parseAccepted check the search is limit accepted answer or not +func (sp *SearchParser) parseAccepted(query *string) (accepted bool) { + var ( + q = *query + expr = `isaccepted:yes` + ) + + if strings.Contains(q, expr) { + accepted = true + q = strings.ReplaceAll(q, expr, "") + } + + *query = strings.TrimSpace(q) + return +} + +// parseQuestionID check whether specified question's id +func (sp *SearchParser) parseQuestionID(query *string) (questionID string) { + var ( + q = *query + expr = `inquestion:(\d+)` + ) + + re := regexp.MustCompile(expr) + res := re.FindStringSubmatch(q) + if len(res) == 2 { + questionID = res[1] + q = re.ReplaceAllString(q, "") + } + + *query = strings.TrimSpace(q) + return +} + +// parseIsAnswer check the result if only limit answer or not +func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) { + var ( + q = *query + expr = `is:answer` + ) + + if strings.Contains(q, expr) { + isAnswer = true + q = strings.ReplaceAll(q, expr, "") + } + + *query = strings.TrimSpace(q) + return +} diff --git a/internal/service/search_service.go b/internal/service/search_service.go deleted file mode 100644 index 098fac586..000000000 --- a/internal/service/search_service.go +++ /dev/null @@ -1,93 +0,0 @@ -package service - -import ( - "context" - - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/search" - "github.com/answerdev/answer/internal/service/search_common" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - usercommon "github.com/answerdev/answer/internal/service/user_common" -) - -type Search interface { - Parse(dto *schema.SearchDTO) (ok bool) - Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) -} - -type SearchService struct { - searchRepo search_common.SearchRepo - tagSearch *search.TagSearch - withinSearch *search.WithinSearch - authorSearch *search.AuthorSearch - scoreSearch *search.ScoreSearch - answersSearch *search.AnswersSearch - notAcceptedQuestion *search.NotAcceptedQuestion - acceptedAnswerSearch *search.AcceptedAnswerSearch - inQuestionSearch *search.InQuestionSearch - questionSearch *search.QuestionSearch - answerSearch *search.AnswerSearch - viewsSearch *search.ViewsSearch - objectSearch *search.ObjectSearch -} - -func NewSearchService( - searchRepo search_common.SearchRepo, - tagRepo tagcommon.TagRepo, - userCommon *usercommon.UserCommon, - followCommon activity_common.FollowRepo, -) *SearchService { - return &SearchService{ - searchRepo: searchRepo, - tagSearch: search.NewTagSearch(searchRepo, tagRepo, followCommon), - withinSearch: search.NewWithinSearch(searchRepo), - authorSearch: search.NewAuthorSearch(searchRepo, userCommon), - scoreSearch: search.NewScoreSearch(searchRepo), - answersSearch: search.NewAnswersSearch(searchRepo), - acceptedAnswerSearch: search.NewAcceptedAnswerSearch(searchRepo), - notAcceptedQuestion: search.NewNotAcceptedQuestion(searchRepo), - inQuestionSearch: search.NewInQuestionSearch(searchRepo), - questionSearch: search.NewQuestionSearch(searchRepo), - answerSearch: search.NewAnswerSearch(searchRepo), - viewsSearch: search.NewViewsSearch(searchRepo), - objectSearch: search.NewObjectSearch(searchRepo), - } -} - -func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) { - extra = nil - if dto.Page < 1 { - dto.Page = 1 - } - - switch { - case ss.tagSearch.Parse(dto): - resp, total, err = ss.tagSearch.Search(ctx) - extra = ss.tagSearch.Extra - case ss.withinSearch.Parse(dto): - resp, total, err = ss.withinSearch.Search(ctx) - case ss.authorSearch.Parse(dto): - resp, total, err = ss.authorSearch.Search(ctx) - case ss.scoreSearch.Parse(dto): - resp, total, err = ss.scoreSearch.Search(ctx) - case ss.answersSearch.Parse(dto): - resp, total, err = ss.answersSearch.Search(ctx) - case ss.acceptedAnswerSearch.Parse(dto): - resp, total, err = ss.acceptedAnswerSearch.Search(ctx) - case ss.notAcceptedQuestion.Parse(dto): - resp, total, err = ss.notAcceptedQuestion.Search(ctx) - case ss.inQuestionSearch.Parse(dto): - resp, total, err = ss.inQuestionSearch.Search(ctx) - case ss.questionSearch.Parse(dto): - resp, total, err = ss.questionSearch.Search(ctx) - case ss.answerSearch.Parse(dto): - resp, total, err = ss.answerSearch.Search(ctx) - case ss.viewsSearch.Parse(dto): - resp, total, err = ss.viewsSearch.Search(ctx) - default: - ss.objectSearch.Parse(dto) - resp, total, err = ss.objectSearch.Search(ctx) - } - return resp, total, extra, err -} diff --git a/internal/service/service_config/service_config.go b/internal/service/service_config/service_config.go index 33e1f6268..90e399e17 100644 --- a/internal/service/service_config/service_config.go +++ b/internal/service/service_config/service_config.go @@ -1,7 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package service_config type ServiceConfig struct { - SecretKey string `json:"secret_key" mapstructure:"secret_key"` - WebHost string `json:"web_host" mapstructure:"web_host"` - UploadPath string `json:"upload_path" mapstructure:"upload_path"` + UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"` + CleanUpUploads bool `json:"clean_up_uploads" mapstructure:"clean_up_uploads" yaml:"clean_up_uploads"` + CleanOrphanUploadsPeriodHours int `json:"clean_orphan_uploads_period_hours" mapstructure:"clean_orphan_uploads_period_hours" yaml:"clean_orphan_uploads_period_hours"` + PurgeDeletedFilesPeriodDays int `json:"purge_deleted_files_period_days" mapstructure:"purge_deleted_files_period_days" yaml:"purge_deleted_files_period_days"` } diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go new file mode 100644 index 000000000..a0f4891c4 --- /dev/null +++ b/internal/service/siteinfo/siteinfo_service.go @@ -0,0 +1,489 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package siteinfo + +import ( + "context" + "encoding/json" + errpkg "errors" + "fmt" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/config" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/file_record" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/internal/service/siteinfo_common" + tagcommon "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/plugin" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +type SiteInfoService struct { + siteInfoRepo siteinfo_common.SiteInfoRepo + siteInfoCommonService siteinfo_common.SiteInfoCommonService + emailService *export.EmailService + tagCommonService *tagcommon.TagCommonService + configService *config.ConfigService + questioncommon *questioncommon.QuestionCommon + fileRecordService *file_record.FileRecordService +} + +func NewSiteInfoService( + siteInfoRepo siteinfo_common.SiteInfoRepo, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + emailService *export.EmailService, + tagCommonService *tagcommon.TagCommonService, + configService *config.ConfigService, + questioncommon *questioncommon.QuestionCommon, + fileRecordService *file_record.FileRecordService, + +) *SiteInfoService { + plugin.RegisterGetSiteURLFunc(func() string { + generalSiteInfo, err := siteInfoCommonService.GetSiteGeneral(context.Background()) + if err != nil { + log.Error(err) + return "" + } + return generalSiteInfo.SiteUrl + }) + + return &SiteInfoService{ + siteInfoRepo: siteInfoRepo, + siteInfoCommonService: siteInfoCommonService, + emailService: emailService, + tagCommonService: tagCommonService, + configService: configService, + questioncommon: questioncommon, + fileRecordService: fileRecordService, + } +} + +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + return s.siteInfoCommonService.GetSiteGeneral(ctx) +} + +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + return s.siteInfoCommonService.GetSiteInterface(ctx) +} + +// GetSiteBranding get site info branding +func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { + return s.siteInfoCommonService.GetSiteBranding(ctx) +} + +// GetSiteUsers get site info about users +func (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { + return s.siteInfoCommonService.GetSiteUsers(ctx) +} + +// GetSiteWrite get site info write +func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) { + resp = &schema.SiteWriteResp{} + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite) + if err != nil { + log.Error(err) + return resp, nil + } + if exist { + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + } + + resp.RecommendTags, err = s.tagCommonService.GetSiteWriteRecommendTag(ctx) + if err != nil { + log.Error(err) + } + resp.ReservedTags, err = s.tagCommonService.GetSiteWriteReservedTag(ctx) + if err != nil { + log.Error(err) + } + return resp, nil +} + +// GetSiteLegal get site legal info +func (s *SiteInfoService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) { + return s.siteInfoCommonService.GetSiteLegal(ctx) +} + +// GetSiteLogin get site login info +func (s *SiteInfoService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) { + return s.siteInfoCommonService.GetSiteLogin(ctx) +} + +// GetSiteCustomCssHTML get site custom css html config +func (s *SiteInfoService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) { + return s.siteInfoCommonService.GetSiteCustomCssHTML(ctx) +} + +// GetSiteTheme get site theme config +func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { + return s.siteInfoCommonService.GetSiteTheme(ctx) +} + +func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { + req.FormatSiteUrl() + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeGeneral, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data) +} + +func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { + // check language + if !translator.CheckLanguageIsValid(req.Language) { + err = errors.BadRequest(reason.LangNotFound) + return + } + + content, _ := json.Marshal(req) + data := entity.SiteInfo{ + Type: constant.SiteTypeInterface, + Content: string(content), + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterface, &data) +} + +// SaveSiteBranding save site branding information +func (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.SiteBrandingReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeBranding, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data) +} + +// SaveSiteWrite save site configuration about write +func (s *SiteInfoService) SaveSiteWrite(ctx context.Context, req *schema.SiteWriteReq) (resp interface{}, err error) { + recommendTags, reservedTags := make([]string, 0), make([]string, 0) + recommendTagMapping, reservedTagMapping := make(map[string]bool), make(map[string]bool) + for _, tag := range req.ReservedTags { + if !recommendTagMapping[tag.SlugName] { + reservedTagMapping[tag.SlugName] = true + reservedTags = append(reservedTags, tag.SlugName) + } + } + + // recommend tags can't contain reserved tag + for _, tag := range req.RecommendTags { + if reservedTagMapping[tag.SlugName] { + continue + } + if !recommendTagMapping[tag.SlugName] { + recommendTagMapping[tag.SlugName] = true + recommendTags = append(recommendTags, tag.SlugName) + } + } + errData, err := s.tagCommonService.SetSiteWriteTag(ctx, recommendTags, reservedTags, req.UserID) + if err != nil { + return errData, err + } + + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeWrite, + Content: string(content), + Status: 1, + } + return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeWrite, data) +} + +// SaveSiteLegal save site legal configuration +func (s *SiteInfoService) SaveSiteLegal(ctx context.Context, req *schema.SiteLegalReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeLegal, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLegal, data) +} + +// SaveSiteLogin save site legal configuration +func (s *SiteInfoService) SaveSiteLogin(ctx context.Context, req *schema.SiteLoginReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeLogin, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLogin, data) +} + +// SaveSiteCustomCssHTML save site custom html configuration +func (s *SiteInfoService) SaveSiteCustomCssHTML(ctx context.Context, req *schema.SiteCustomCssHTMLReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeCustomCssHTML, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeCustomCssHTML, data) +} + +// SaveSiteTheme save site custom html configuration +func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThemeReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeTheme, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data) +} + +// SaveSiteUsers save site users +func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) { + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypeUsers, + Content: string(content), + Status: 1, + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data) +} + +// GetSMTPConfig get smtp config +func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (resp *schema.GetSMTPConfigResp, err error) { + emailConfig, err := s.emailService.GetEmailConfig(ctx) + if err != nil { + return nil, err + } + resp = &schema.GetSMTPConfigResp{} + _ = copier.Copy(resp, emailConfig) + resp.SMTPPassword = strings.Repeat("*", len(resp.SMTPPassword)) + return resp, nil +} + +// UpdateSMTPConfig get smtp config +func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.UpdateSMTPConfigReq) (err error) { + emailConfig, err := s.emailService.GetEmailConfig(ctx) + if err != nil { + return err + } + + ec := &export.EmailConfig{} + _ = copier.Copy(ec, req) + + if len(ec.SMTPPassword) > 0 && ec.SMTPPassword == strings.Repeat("*", len(ec.SMTPPassword)) { + ec.SMTPPassword = emailConfig.SMTPPassword + } + + err = s.emailService.SetEmailConfig(ctx, ec) + if err != nil { + return err + } + if len(req.TestEmailRecipient) > 0 { + title, body, err := s.emailService.TestTemplate(ctx) + if err != nil { + return err + } + go s.emailService.Send(ctx, req.TestEmailRecipient, title, body) + } + return nil +} + +func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) { + resp = &schema.SiteSeoReq{} + if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil { + return resp, err + } + loginConfig, err := s.GetSiteLogin(ctx) + if err != nil { + log.Error(err) + return resp, nil + } + // If the site is set to privacy mode, prohibit crawling any page. + if loginConfig.LoginRequired { + resp.Robots = "User-agent: *\nDisallow: /" + return resp, nil + } + return resp, nil +} + +func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) { + content, _ := json.Marshal(req) + data := entity.SiteInfo{ + Type: constant.SiteTypeSeo, + Content: string(content), + } + return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data) +} + +func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) { + privilege := &schema.UpdatePrivilegesConfigReq{} + if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil { + return nil, err + } + privilegeOptions := schema.DefaultPrivilegeOptions + if privilege.CustomPrivileges != nil && len(privilege.CustomPrivileges) > 0 { + privilegeOptions = append(privilegeOptions, &schema.PrivilegeOption{ + Level: schema.PrivilegeLevelCustom, + LevelDesc: reason.PrivilegeLevelCustomDesc, + Privileges: privilege.CustomPrivileges, + }) + } else { + privilegeOptions = append(privilegeOptions, schema.DefaultCustomPrivilegeOption) + } + resp = &schema.GetPrivilegesConfigResp{ + Options: s.translatePrivilegeOptions(ctx, privilegeOptions), + SelectedLevel: schema.PrivilegeLevel3, + } + if privilege != nil && privilege.Level > 0 { + resp.SelectedLevel = privilege.Level + } + return resp, nil +} + +func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context, privilegeOptions []*schema.PrivilegeOption) (options []*schema.PrivilegeOption) { + la := handler.GetLangByCtx(ctx) + for _, option := range privilegeOptions { + op := &schema.PrivilegeOption{ + Level: option.Level, + LevelDesc: translator.Tr(la, option.LevelDesc), + } + for _, privilege := range option.Privileges { + op.Privileges = append(op.Privileges, &constant.Privilege{ + Key: privilege.Key, + Label: translator.Tr(la, privilege.Label), + Value: privilege.Value, + }) + } + options = append(options, op) + } + return +} + +func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) { + var choosePrivileges []*constant.Privilege + if req.Level == schema.PrivilegeLevelCustom { + choosePrivileges = req.CustomPrivileges + } else { + chooseOption := schema.DefaultPrivilegeOptions.Choose(req.Level) + if chooseOption == nil { + return nil + } + choosePrivileges = chooseOption.Privileges + } + if choosePrivileges == nil { + return nil + } + + // update site info that user choose which privilege level + if req.Level == schema.PrivilegeLevelCustom { + privilegeMap := make(map[string]int) + for _, privilege := range req.CustomPrivileges { + privilegeMap[privilege.Key] = privilege.Value + } + var privileges []*constant.Privilege + for _, privilege := range constant.RankAllPrivileges { + privileges = append(privileges, &constant.Privilege{ + Key: privilege.Key, + Label: privilege.Label, + Value: privilegeMap[privilege.Key], + }) + } + req.CustomPrivileges = privileges + } else { + privilege := &schema.UpdatePrivilegesConfigReq{} + if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil { + return err + } + req.CustomPrivileges = privilege.CustomPrivileges + } + + content, _ := json.Marshal(req) + data := &entity.SiteInfo{ + Type: constant.SiteTypePrivileges, + Content: string(content), + Status: 1, + } + err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data) + if err != nil { + return err + } + + // update privilege in config + for _, privilege := range choosePrivileges { + err = s.configService.UpdateConfig(ctx, privilege.Key, fmt.Sprintf("%d", privilege.Value)) + if err != nil { + return err + } + } + return +} + +func (s *SiteInfoService) CleanUpRemovedBrandingFiles( + ctx context.Context, + newBranding *schema.SiteBrandingReq, + currentBranding *schema.SiteBrandingResp, +) error { + var allErrors []error + currentFiles := map[string]string{ + "logo": currentBranding.Logo, + "mobile_logo": currentBranding.MobileLogo, + "square_icon": currentBranding.SquareIcon, + "favicon": currentBranding.Favicon, + } + + newFiles := map[string]string{ + "logo": newBranding.Logo, + "mobile_logo": newBranding.MobileLogo, + "square_icon": newBranding.SquareIcon, + "favicon": newBranding.Favicon, + } + + for key, currentFile := range currentFiles { + newFile := newFiles[key] + if currentFile != "" && currentFile != newFile { + fileRecord, err := s.fileRecordService.GetFileRecordByURL(ctx, currentFile) + if err != nil { + allErrors = append(allErrors, err) + continue + } + if fileRecord == nil { + err := errpkg.New("file record is nil for key " + key) + allErrors = append(allErrors, err) + continue + } + if err := s.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { + allErrors = append(allErrors, err) + } + } + } + if len(allErrors) > 0 { + return errpkg.Join(allErrors...) + } + return nil +} diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go deleted file mode 100644 index ff9066b09..000000000 --- a/internal/service/siteinfo_common/siteinfo.go +++ /dev/null @@ -1,11 +0,0 @@ -package siteinfo_common - -import ( - "context" - "github.com/answerdev/answer/internal/entity" -) - -type SiteInfoRepo interface { - SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) - GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) -} diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go new file mode 100644 index 000000000..ef4869cc4 --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package siteinfo_common + +import ( + "context" + "encoding/json" + "html" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/pkg/gravatar" + "github.com/segmentfault/pacman/log" +) + +//go:generate mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock +type SiteInfoRepo interface { + SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) + GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) + IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) +} + +// siteInfoCommonService site info common service +type siteInfoCommonService struct { + siteInfoRepo SiteInfoRepo +} + +type SiteInfoCommonService interface { + GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) + GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) + GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) + GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) + FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo + FormatListAvatar(ctx context.Context, userList []*entity.User) (userID2AvatarMapping map[string]*schema.AvatarInfo) + GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) + GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) + GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) + GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) + GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) + GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) + GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) + IsBrandingFileUsed(ctx context.Context, filePath string) bool +} + +// NewSiteInfoCommonService new site info common service +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService { + return &siteInfoCommonService{ + siteInfoRepo: siteInfoRepo, + } +} + +// GetSiteGeneral get site info general +func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + resp = &schema.SiteGeneralResp{CheckUpdate: true} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { + return nil, err + } + resp.Name = html.UnescapeString(resp.Name) + return resp, nil +} + +// GetSiteInterface get site info interface +func (s *siteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + resp = &schema.SiteInterfaceResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteBranding get site info branding +func (s *siteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) { + resp = &schema.SiteBrandingResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteUsers get site info about users +func (s *siteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) { + resp = &schema.SiteUsersResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil { + return nil, err + } + return resp, nil +} + +// FormatAvatar format avatar +func (s *siteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string, userStatus int) *schema.AvatarInfo { + gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) + return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email, userStatus) +} + +// FormatListAvatar format avatar +func (s *siteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) ( + avatarMapping map[string]*schema.AvatarInfo) { + gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) + avatarMapping = make(map[string]*schema.AvatarInfo) + for _, user := range userList { + avatarMapping[user.ID] = s.selectedAvatar(user.Avatar, defaultAvatar, gravatarBaseURL, user.EMail, user.Status) + } + return avatarMapping +} + +func (s *siteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) { + gravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar + usersConfig, err := s.GetSiteInterface(ctx) + if err != nil { + log.Error(err) + } + if len(usersConfig.GravatarBaseURL) > 0 { + gravatarBaseURL = usersConfig.GravatarBaseURL + } + if len(usersConfig.DefaultAvatar) > 0 { + defaultAvatar = usersConfig.DefaultAvatar + } + return gravatarBaseURL, defaultAvatar +} + +func (s *siteInfoCommonService) selectedAvatar( + originalAvatarData, + defaultAvatar, gravatarBaseURL, + email string, userStatus int) *schema.AvatarInfo { + avatarInfo := &schema.AvatarInfo{} + _ = json.Unmarshal([]byte(originalAvatarData), avatarInfo) + + if userStatus == entity.UserStatusDeleted { + return &schema.AvatarInfo{ + Type: constant.DefaultAvatar, + } + } + + if len(avatarInfo.Type) == 0 && defaultAvatar == constant.AvatarTypeGravatar { + avatarInfo.Type = constant.AvatarTypeGravatar + avatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email) + } else if avatarInfo.Type == constant.AvatarTypeGravatar { + avatarInfo.Gravatar = gravatar.GetAvatarURL(gravatarBaseURL, email) + } + return avatarInfo +} + +// GetSiteWrite get site info write +func (s *siteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) { + resp = &schema.SiteWriteResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteLegal get site info write +func (s *siteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) { + resp = &schema.SiteLegalResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteLogin get site login config +func (s *siteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) { + resp = &schema.SiteLoginResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteCustomCssHTML get site custom css html config +func (s *siteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) { + resp = &schema.SiteCustomCssHTMLResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil { + return nil, err + } + return resp, nil +} + +// GetSiteTheme get site theme +func (s *siteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) { + resp = &schema.SiteThemeResp{ + ThemeOptions: schema.GetThemeOptions, + } + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil { + return nil, err + } + resp.TrTheme(ctx) + return resp, nil +} + +// GetSiteSeo get site seo +func (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) { + resp = &schema.SiteSeoResp{} + if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil { + return nil, err + } + return resp, nil +} + +func (s *siteInfoCommonService) EnableShortID(ctx context.Context) (enabled bool) { + siteSeo, err := s.GetSiteSeo(ctx) + if err != nil { + log.Error(err) + return false + } + return siteSeo.IsShortLink() +} + +func (s *siteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType) + if err != nil { + return err + } + if !exist { + return nil + } + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return nil +} + +func (s *siteInfoCommonService) IsBrandingFileUsed(ctx context.Context, filePath string) bool { + used, err := s.siteInfoRepo.IsBrandingFileUsed(ctx, filePath) + if err != nil { + log.Errorf("error checking if branding file is used: %v", err) + // will try again with the next clean up + return true + } + return used +} diff --git a/internal/service/siteinfo_common/siteinfo_service_test.go b/internal/service/siteinfo_common/siteinfo_service_test.go new file mode 100644 index 000000000..3a567dcb9 --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service_test.go @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package siteinfo_common + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/service/mock" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +var ( + mockSiteInfoRepo *mock.MockSiteInfoRepo +) + +func mockInit(ctl *gomock.Controller) { + mockSiteInfoRepo = mock.NewMockSiteInfoRepo(ctl) + mockSiteInfoRepo.EXPECT().GetByType(gomock.Any(), constant.SiteTypeGeneral). + Return(&entity.SiteInfo{Content: `{"name":"name"}`}, true, nil) +} + +func TestSiteInfoCommonService_GetSiteGeneral(t *testing.T) { + ctl := gomock.NewController(t) + defer ctl.Finish() + mockInit(ctl) + siteInfoCommonService := NewSiteInfoCommonService(mockSiteInfoRepo) + resp, err := siteInfoCommonService.GetSiteGeneral(context.TODO()) + assert.NoError(t, err) + assert.Equal(t, resp.Name, "name") +} diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go deleted file mode 100644 index f1ae6e8da..000000000 --- a/internal/service/siteinfo_service.go +++ /dev/null @@ -1,153 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/export" - "github.com/answerdev/answer/internal/service/siteinfo_common" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" -) - -type SiteInfoService struct { - siteInfoRepo siteinfo_common.SiteInfoRepo - emailService *export.EmailService -} - -func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService *export.EmailService) *SiteInfoService { - return &SiteInfoService{ - siteInfoRepo: siteInfoRepo, - emailService: emailService, - } -} - -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) - if !exist { - return - } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return -} - -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) - if !exist { - return - } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return -} - -func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { - var ( - siteType = "general" - content []byte - ) - content, err = json.Marshal(req) - - data := entity.SiteInfo{ - Type: siteType, - Content: string(content), - } - - err = s.siteInfoRepo.SaveByType(ctx, siteType, &data) - return -} - -func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { - var ( - siteType = "interface" - themeExist, - langExist bool - content []byte - ) - - // check theme - for _, theme := range schema.GetThemeOptions { - if theme.Value == req.Theme { - themeExist = true - break - } - } - if !themeExist { - err = errors.BadRequest(reason.ThemeNotFound) - return - } - - // check language - for _, lang := range schema.GetLangOptions { - if lang.Value == req.Language { - langExist = true - break - } - } - if !langExist { - err = errors.BadRequest(reason.LangNotFound) - return - } - - content, err = json.Marshal(req) - - data := entity.SiteInfo{ - Type: siteType, - Content: string(content), - } - - err = s.siteInfoRepo.SaveByType(ctx, siteType, &data) - return -} - -// GetSMTPConfig get smtp config -func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) ( - resp *schema.GetSMTPConfigResp, err error) { - emailConfig, err := s.emailService.GetEmailConfig() - if err != nil { - return nil, err - } - resp = &schema.GetSMTPConfigResp{} - _ = copier.Copy(resp, emailConfig) - return resp, nil -} - -// UpdateSMTPConfig get smtp config -func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.UpdateSMTPConfigReq) (err error) { - oldEmailConfig, err := s.emailService.GetEmailConfig() - if err != nil { - return err - } - _ = copier.Copy(oldEmailConfig, req) - - err = s.emailService.SetEmailConfig(oldEmailConfig) - if err != nil { - return err - } - if len(req.TestEmailRecipient) > 0 { - title, body, err := s.emailService.TestTemplate(ctx) - if err != nil { - return err - } - go s.emailService.Send(ctx, req.TestEmailRecipient, title, body, "", "") - } - return -} diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index cf6fc02e9..577199ec8 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package tag import ( @@ -5,109 +24,118 @@ import ( "encoding/json" "strings" - "github.com/answerdev/answer/internal/service/revision_common" - - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/permission" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/siteinfo_common" + tagcommonser "github.com/apache/answer/internal/service/tag_common" + "github.com/apache/answer/pkg/htmltext" "github.com/jinzhu/copier" + + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_common" + "github.com/apache/answer/internal/service/permission" + "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) // TagService user service type TagService struct { - tagRepo tagcommon.TagRepo - revisionService *revision_common.RevisionService - followCommon activity_common.FollowRepo + tagRepo tagcommonser.TagRepo + tagCommonService *tagcommonser.TagCommonService + revisionService *revision_common.RevisionService + followCommon activity_common.FollowRepo + siteInfoService siteinfo_common.SiteInfoCommonService + activityQueueService activity_queue.ActivityQueueService } // NewTagService new tag service func NewTagService( - tagRepo tagcommon.TagRepo, + tagRepo tagcommonser.TagRepo, + tagCommonService *tagcommonser.TagCommonService, revisionService *revision_common.RevisionService, - followCommon activity_common.FollowRepo) *TagService { + followCommon activity_common.FollowRepo, + siteInfoService siteinfo_common.SiteInfoCommonService, + activityQueueService activity_queue.ActivityQueueService, +) *TagService { return &TagService{ - tagRepo: tagRepo, - revisionService: revisionService, - followCommon: followCommon, + tagRepo: tagRepo, + tagCommonService: tagCommonService, + revisionService: revisionService, + followCommon: followCommon, + siteInfoService: siteInfoService, + activityQueueService: activityQueueService, } } -// SearchTagLike get tag list all -func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []string, err error) { - tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5) +// RemoveTag delete tag +func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (err error) { + //If the tag has associated problems, it cannot be deleted + tagCount, err := ts.tagCommonService.CountTagRelByTagID(ctx, req.TagID) if err != nil { - return + return err } - for _, tag := range tags { - resp = append(resp, tag.SlugName) + if tagCount > 0 { + return errors.BadRequest(reason.TagIsUsedCannotDelete) } - return resp, nil -} -// RemoveTag delete tag -func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) { - // TODO permission + //If the tag has associated problems, it cannot be deleted + tagSynonymCount, err := ts.tagRepo.GetTagSynonymCount(ctx, req.TagID) + if err != nil { + return err + } + if tagSynonymCount > 0 { + return errors.BadRequest(reason.TagIsUsedCannotDelete) + } - err = ts.tagRepo.RemoveTag(ctx, tagID) + // tagRelRepo + err = ts.tagRepo.RemoveTag(ctx, req.TagID) if err != nil { return err } + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: req.TagID, + OriginalObjectID: req.TagID, + ActivityTypeKey: constant.ActTagDeleted, + }) return nil } // UpdateTag update tag func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { - tag := &entity.Tag{} - _ = copier.Copy(tag, req) - tag.ID = req.TagID - err = ts.tagRepo.UpdateTag(ctx, tag) - if err != nil { - return err - } + return ts.tagCommonService.UpdateTag(ctx, req) +} - tagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID) +// RecoverTag recover tag +func (ts *TagService) RecoverTag(ctx context.Context, req *schema.RecoverTagReq) (err error) { + tagInfo, exist, err := ts.tagRepo.MustGetTagByNameOrID(ctx, req.TagID, "") if err != nil { return err } if !exist { return errors.BadRequest(reason.TagNotFound) } - if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 { - log.Debugf("tag %s update slug_name", tagInfo.SlugName) - tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) - if err != nil { - return err - } - updateTagSlugNames := make([]string, 0) - for _, tag := range tagList { - updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) - } - err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) - if err != nil { - return err - } + if tagInfo.Status != entity.TagStatusDeleted { + return nil } - revisionDTO := &schema.AddRevisionDTO{ - UserID: req.UserID, - ObjectID: tag.ID, - Title: tag.SlugName, - Log: req.EditSummary, - } - tagInfoJson, _ := json.Marshal(tagInfo) - revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) + err = ts.tagRepo.RecoverTag(ctx, req.TagID) if err != nil { return err } - return + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + TriggerUserID: converter.StringToInt64(req.UserID), + ObjectID: req.TagID, + OriginalObjectID: req.TagID, + ActivityTypeKey: constant.ActTagUndeleted, + }) + return nil } // GetTagInfo get tag one @@ -117,26 +145,30 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) exist bool ) if len(req.ID) > 0 { - tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, req.ID) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID) } else { - tagInfo, exist, err = ts.tagRepo.GetTagBySlugName(ctx, req.Name) + tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name) + } + // If user can recover deleted tag, try to search in all tags including deleted tags + if !exist && req.CanRecover { + tagInfo, exist, err = ts.tagRepo.MustGetTagByNameOrID(ctx, req.ID, req.Name) } if err != nil { return nil, err } if !exist { - return nil, errors.BadRequest(reason.TagNotFound) + return nil, errors.NotFound(reason.TagNotFound) } resp = &schema.GetTagResp{} // if tag is synonyms get original tag info if tagInfo.MainTagID > 0 { - tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } if !exist { - return nil, errors.BadRequest(reason.TagNotFound) + return nil, errors.NotFound(reason.TagNotFound) } resp.MainTagSlugName = tagInfo.SlugName } @@ -147,14 +179,38 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) resp.DisplayName = tagInfo.DisplayName resp.OriginalText = tagInfo.OriginalText resp.ParsedText = tagInfo.ParsedText + resp.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240) resp.FollowCount = tagInfo.FollowCount resp.QuestionCount = tagInfo.QuestionCount + resp.Recommend = tagInfo.Recommend + resp.Reserved = tagInfo.Reserved resp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID) - resp.MemberActions = permission.GetTagPermission(req.UserID, req.UserID) + resp.Status = entity.TagStatusDisplayMapping[tagInfo.Status] + resp.MemberActions = permission.GetTagPermission(ctx, tagInfo.Status, req.CanEdit, req.CanDelete, req.CanMerge, req.CanRecover) resp.GetExcerpt() return resp, nil } +// GetTagsBySlugName get tags by slug name +func (ts *TagService) GetTagsBySlugName(ctx context.Context, req *schema.SearchTagsBySlugName) ( + resp []*schema.GetTagBasicResp, err error) { + resp = make([]*schema.GetTagBasicResp, 0) + tagSlugNames := strings.Split(req.Tags, ",") + if len(tagSlugNames) == 0 { + return resp, nil + } + tagList, err := ts.tagCommonService.GetTagListByNames(ctx, tagSlugNames) + if err != nil { + return resp, err + } + for _, tag := range tagList { + tagItem := &schema.GetTagBasicResp{} + _ = copier.Copy(tagItem, tag) + resp = append(resp, tagItem) + } + return resp, nil +} + // GetFollowingTags get following tags func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( resp []*schema.GetFollowingTagsResp, err error) { @@ -166,7 +222,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( if err != nil { return nil, err } - tagList, err := ts.tagRepo.GetTagListByIDs(ctx, objIDs) + tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } @@ -175,9 +231,11 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( TagID: t.ID, SlugName: t.SlugName, DisplayName: t.DisplayName, + Recommend: t.Recommend, + Reserved: t.Reserved, } if t.MainTagID > 0 { - mainTag, exist, err := ts.tagRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) + mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } @@ -192,8 +250,9 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( // GetTagSynonyms get tag synonyms func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) ( - resp []*schema.GetTagSynonymsResp, err error) { - tag, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID) + resp *schema.GetTagSynonymsResp, err error) { + resp = &schema.GetTagSynonymsResp{Synonyms: make([]*schema.TagSynonym, 0)} + tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return } @@ -224,15 +283,15 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno mainTagSlugName = tag.SlugName } - resp = make([]*schema.GetTagSynonymsResp, 0) for _, t := range tagList { - resp = append(resp, &schema.GetTagSynonymsResp{ + resp.Synonyms = append(resp.Synonyms, &schema.TagSynonym{ TagID: t.ID, SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, }) } + resp.MemberActions = permission.GetTagSynonymPermission(ctx, req.CanEdit) return } @@ -242,7 +301,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa req.Format() addSynonymTagList := make([]string, 0) removeSynonymTagList := make([]string, 0) - mainTagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID) + mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -252,9 +311,12 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa // find all exist tag for _, item := range req.SynonymTagList { + if item.SlugName == mainTagInfo.SlugName { + return errors.BadRequest(reason.TagCannotSetSynonymAsItself) + } addSynonymTagList = append(addSynonymTagList, item.SlugName) } - tagListInDB, err := ts.tagRepo.GetTagListByNames(ctx, addSynonymTagList) + tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } @@ -275,11 +337,12 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable + item.UserID = req.UserID needAddTagList = append(needAddTagList, item) } if len(needAddTagList) > 0 { - err = ts.tagRepo.AddTagList(ctx, needAddTagList) + err = ts.tagCommonService.AddTagList(ctx, needAddTagList) if err != nil { return err } @@ -293,10 +356,17 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: tag.ID, + OriginalObjectID: tag.ID, + ActivityTypeKey: constant.ActTagCreated, + RevisionID: revisionID, + }) } } @@ -333,50 +403,105 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWithPageReq) (pageModel *pager.PageModel, err error) { tag := &entity.Tag{} _ = copier.Copy(tag, req) + tag.UserID = "" page := req.Page pageSize := req.PageSize - tags, total, err := ts.tagRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) + tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } resp := make([]*schema.GetTagPageResp, 0) for _, tag := range tags { - resp = append(resp, &schema.GetTagPageResp{ + item := &schema.GetTagPageResp{ TagID: tag.ID, SlugName: tag.SlugName, + Description: htmltext.FetchExcerpt(tag.ParsedText, "...", 240), DisplayName: tag.DisplayName, - OriginalText: cutOutTagParsedText(tag.OriginalText), - ParsedText: cutOutTagParsedText(tag.ParsedText), + OriginalText: tag.OriginalText, + ParsedText: tag.ParsedText, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID), CreatedAt: tag.CreatedAt.Unix(), UpdatedAt: tag.UpdatedAt.Unix(), - }) + Recommend: tag.Recommend, + Reserved: tag.Reserved, + } + item.GetExcerpt() + resp = append(resp, item) } return pager.NewPageModel(total, resp), nil } +// MergeTag merge tag +func (ts *TagService) MergeTag(ctx context.Context, req *schema.MergeTagReq) (err error) { + // 1. get source tag and its synonyms + sourceTag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.SourceTagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + + sourceTagSynonyms, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(sourceTag.ID)}) + if err != nil { + return err + } + + addSynonymTagList := make([]string, 0) + addSynonymTagList = append(addSynonymTagList, sourceTag.SlugName) + for _, tag := range sourceTagSynonyms { + addSynonymTagList = append(addSynonymTagList, tag.SlugName) + } + + // 2. get target tag + targetTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TargetTagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + + // 3. update source tag and its synonyms as synonyms of target tag + if len(addSynonymTagList) > 0 { + err = ts.tagRepo.UpdateTagSynonym(ctx, addSynonymTagList, converter.StringToInt64(targetTagInfo.ID), targetTagInfo.SlugName) + if err != nil { + return err + } + } + + // 4. update tag followers + err = ts.followCommon.MigrateFollowers(ctx, sourceTag.ID, targetTagInfo.ID, "follow") + if err != nil { + return err + } + + // 5. update question tags + err = ts.tagCommonService.MigrateTagQuestions(ctx, sourceTag.ID, targetTagInfo.ID) + if err != nil { + return err + } + err = ts.tagCommonService.RefreshTagQuestionCount(ctx, []string{targetTagInfo.ID, sourceTag.ID}) + if err != nil { + return err + } + + return nil +} + // checkTagIsFollow get tag list page func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string) bool { if len(userID) == 0 { return false } - followed, err := ts.followCommon.IsFollowed(userID, tagID) + followed, err := ts.followCommon.IsFollowed(ctx, userID, tagID) if err != nil { log.Error(err) } return followed } - -func cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index dd8353eba..318eac20a 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -1,133 +1,650 @@ -package tagcommon +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package tag_common import ( "context" "encoding/json" + "fmt" + "sort" "strings" - "github.com/answerdev/answer/internal/service/revision_common" - - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/validator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity_queue" + "github.com/apache/answer/internal/service/revision_common" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/converter" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) -type TagRepo interface { +type TagCommonRepo interface { AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) - GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error) + GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) + GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error) + GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error) + GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) + GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) + UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) + UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) +} + +type TagRepo interface { RemoveTag(ctx context.Context, tagID string) (err error) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) - UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) + RecoverTag(ctx context.Context, tagID string) (err error) + MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (tag *entity.Tag, exist bool, err error) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error) - GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) + GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) + GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) - GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error) } type TagRelRepo interface { AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) + RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) + RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) + ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) + HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) - RemoveTagRelListByObjectID(ctx context.Context, objectId string) (err error) - EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) + EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) + GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) + MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error } // TagCommonService user service type TagCommonService struct { - revisionService *revision_common.RevisionService - tagRepo TagRepo - tagRelRepo TagRelRepo + revisionService *revision_common.RevisionService + tagCommonRepo TagCommonRepo + tagRelRepo TagRelRepo + tagRepo TagRepo + siteInfoService siteinfo_common.SiteInfoCommonService + activityQueueService activity_queue.ActivityQueueService } // NewTagCommonService new tag service -func NewTagCommonService(tagRepo TagRepo, tagRelRepo TagRelRepo, - revisionService *revision_common.RevisionService) *TagCommonService { +func NewTagCommonService( + tagCommonRepo TagCommonRepo, + tagRelRepo TagRelRepo, + tagRepo TagRepo, + revisionService *revision_common.RevisionService, + siteInfoService siteinfo_common.SiteInfoCommonService, + activityQueueService activity_queue.ActivityQueueService, +) *TagCommonService { return &TagCommonService{ - tagRepo: tagRepo, - tagRelRepo: tagRelRepo, - revisionService: revisionService, + tagCommonRepo: tagCommonRepo, + tagRelRepo: tagRelRepo, + tagRepo: tagRepo, + revisionService: revisionService, + siteInfoService: siteInfoService, + activityQueueService: activityQueueService, + } +} + +// SearchTagLike get tag list all +func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.GetTagBasicResp, err error) { + tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, len(req.Tag) == 0, false) + if err != nil { + return + } + ts.TagsFormatRecommendAndReserved(ctx, tags) + mainTagId := make([]string, 0) + for _, tag := range tags { + if tag.MainTagID != 0 { + mainTagId = append(mainTagId, converter.IntToString(tag.MainTagID)) + } + } + mainTagMap := make(map[string]*entity.Tag) + if len(mainTagId) > 0 { + mainTagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, mainTagId) + if err != nil { + return nil, err + } + for _, tag := range mainTagList { + mainTagMap[tag.ID] = tag + } + } + for _, tag := range tags { + if tag.MainTagID == 0 { + continue + } + mainTagID := converter.IntToString(tag.MainTagID) + if _, ok := mainTagMap[mainTagID]; ok { + tag.ID = mainTagMap[mainTagID].ID + tag.SlugName = mainTagMap[mainTagID].SlugName + tag.DisplayName = mainTagMap[mainTagID].DisplayName + tag.Reserved = mainTagMap[mainTagID].Reserved + tag.Recommend = mainTagMap[mainTagID].Recommend + } + } + resp = make([]schema.GetTagBasicResp, 0) + repetitiveTag := make(map[string]bool) + for _, tag := range tags { + if _, ok := repetitiveTag[tag.SlugName]; !ok { + item := schema.GetTagBasicResp{} + item.TagID = tag.ID + item.SlugName = tag.SlugName + item.DisplayName = tag.DisplayName + item.Recommend = tag.Recommend + item.Reserved = tag.Reserved + resp = append(resp, item) + repetitiveTag[tag.SlugName] = true + } + } + return resp, nil +} + +func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) { + tags = make([]*schema.SiteWriteTag, 0) + list, err := ts.tagCommonRepo.GetRecommendTagList(ctx) + if err != nil { + return tags, err + } + for _, item := range list { + tags = append(tags, &schema.SiteWriteTag{ + SlugName: item.SlugName, + DisplayName: item.DisplayName, + }) + } + return tags, nil +} + +func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []*schema.SiteWriteTag, err error) { + tags = make([]*schema.SiteWriteTag, 0) + list, err := ts.tagCommonRepo.GetReservedTagList(ctx) + if err != nil { + return tags, err + } + for _, item := range list { + tags = append(tags, &schema.SiteWriteTag{ + SlugName: item.SlugName, + DisplayName: item.DisplayName, + }) } + return tags, nil } -// GetTagListByName -func (ts *TagCommonService) GetTagListByName(ctx context.Context, tagName string) (tagInfo *entity.Tag, exist bool, err error) { - tagName = strings.ToLower(tagName) - return ts.tagRepo.GetTagBySlugName(ctx, tagName) +func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, reservedTags []string, userID string) ( + errFields []*validator.FormErrorField, err error) { + recommendErr := ts.CheckTag(ctx, recommendTags, userID) + reservedErr := ts.CheckTag(ctx, reservedTags, userID) + if recommendErr != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "recommend_tags", + ErrorMsg: recommendErr.Error(), + }) + err = recommendErr + } + if reservedErr != nil { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "reserved_tags", + ErrorMsg: reservedErr.Error(), + }) + err = reservedErr + } + if len(errFields) > 0 { + return errFields, err + } + + err = ts.SetTagsAttribute(ctx, recommendTags, "recommend") + if err != nil { + return nil, err + } + err = ts.SetTagsAttribute(ctx, reservedTags, "reserved") + if err != nil { + return nil, err + } + return nil, nil +} + +// SetTagsAttribute +func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) { + var oldTags []*entity.Tag + switch attribute { + case "recommend": + oldTags, err = ts.tagCommonRepo.GetRecommendTagList(ctx) + case "reserved": + oldTags, err = ts.tagCommonRepo.GetReservedTagList(ctx) + default: + return + } + if err != nil { + return err + } + oldTagSlugNameList := make([]string, 0) + for _, tag := range oldTags { + oldTagSlugNameList = append(oldTagSlugNameList, tag.SlugName) + } + + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, oldTagSlugNameList, attribute, false) + if err != nil { + return err + } + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true) + if err != nil { + return err + } + return nil } func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) { for k, tagname := range tagNames { tagNames[k] = strings.ToLower(tagname) } - return ts.tagRepo.GetTagListByNames(ctx, tagNames) + tagList, err := ts.tagCommonRepo.GetTagListByNames(ctx, tagNames) + if err != nil { + return nil, err + } + ts.TagsFormatRecommendAndReserved(ctx, tagList) + return tagList, nil } -// +func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) { + taginfo, err := ts.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return false, err + } + if !taginfo.RequiredTag || len(taginfo.RecommendTags) == 0 { + return true, nil + } + tagNames := make([]string, 0) + for _, item := range tags { + item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-") + tagNames = append(tagNames, item.SlugName) + } + list, err := ts.GetTagListByNames(ctx, tagNames) + if err != nil { + return false, err + } + for _, item := range list { + if item.Recommend { + return true, nil + } + } + return false, nil +} + +func (ts *TagCommonService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) { + tagNames := make([]string, 0) + tagMap := make(map[string]bool) + for _, item := range tags { + item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-") + tagNames = append(tagNames, item.SlugName) + tagMap[item.SlugName] = false + } + list, err := ts.GetTagListByNames(ctx, tagNames) + if err != nil { + return true, err + } + for _, item := range list { + _, ok := tagMap[item.SlugName] + if ok { + tagMap[item.SlugName] = true + } + } + for _, has := range tagMap { + if !has { + return true, nil + } + } + return false, nil +} // GetObjectTag get object tag func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) { - objTags = make([]*schema.TagResp, 0) - tagIDList := make([]string, 0) + tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId) + if err != nil { + return nil, err + } + return ts.TagFormat(ctx, tagsInfoList) +} + +// AddTag get object tag +func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (resp *schema.AddTagResp, err error) { + _, exist, err := ts.GetTagBySlugName(ctx, req.SlugName) + if err != nil { + return nil, err + } + if exist { + return nil, errors.BadRequest(reason.TagAlreadyExist) + } + slugName := strings.ReplaceAll(req.SlugName, " ", "-") + slugName = strings.ToLower(slugName) + tagInfo := &entity.Tag{ + SlugName: slugName, + DisplayName: req.DisplayName, + OriginalText: req.OriginalText, + ParsedText: req.ParsedText, + Status: entity.TagStatusAvailable, + UserID: req.UserID, + } + tagList := []*entity.Tag{tagInfo} + err = ts.tagCommonRepo.AddTagList(ctx, tagList) + if err != nil { + return nil, err + } + revisionDTO := &schema.AddRevisionDTO{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + Title: tagInfo.SlugName, + } + tagInfoJson, _ := json.Marshal(tagInfo) + revisionDTO.Content = string(tagInfoJson) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return nil, err + } + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + OriginalObjectID: tagInfo.ID, + ActivityTypeKey: constant.ActTagCreated, + RevisionID: revisionID, + }) + return &schema.AddTagResp{SlugName: tagInfo.SlugName}, nil +} + +// AddTagList get object tag +func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { + return ts.tagCommonRepo.AddTagList(ctx, tagList) +} + +// GetTagByID get object tag +func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) { + tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID, false) + if !exist { + return + } + ts.tagFormatRecommendAndReserved(ctx, tag) + return +} + +// GetTagIDsByMainTagID get object tag +func (ts *TagCommonService) GetTagIDsByMainTagID(ctx context.Context, tagID string) (tagIDs []string, err error) { + tagIDs, err = ts.tagRepo.GetIDsByMainTagId(ctx, tagID) + return +} + +// GetTagBySlugName get object tag +func (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) { + tag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName) + if !exist { + return + } + ts.tagFormatRecommendAndReserved(ctx, tag) + return +} + +// GetTagListByIDs get object tag +func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { + tagList, err = ts.tagCommonRepo.GetTagListByIDs(ctx, ids) + if err != nil { + return nil, err + } + ts.TagsFormatRecommendAndReserved(ctx, tagList) + return +} + +// GetTagPage get object tag +func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) ( + tagList []*entity.Tag, total int64, err error) { + tagList, total, err = ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, queryCond) + if err != nil { + return nil, 0, err + } + ts.TagsFormatRecommendAndReserved(ctx, tagList) + return +} + +func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) { tagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId) if err != nil { return nil, err } + tagIDList := make([]string, 0) for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList) + objTags, err = ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } - for _, tagInfo := range tagsInfoList { + return objTags, nil +} + +func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (objTags []*schema.TagResp, err error) { + objTags = make([]*schema.TagResp, 0) + for _, tagInfo := range tags { objTags = append(objTags, &schema.TagResp{ SlugName: tagInfo.SlugName, DisplayName: tagInfo.DisplayName, MainTagSlugName: tagInfo.MainTagSlugName, + Recommend: tagInfo.Recommend, + Reserved: tagInfo.Reserved, }) } return objTags, nil } +func (ts *TagCommonService) TagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { + if len(tagList) == 0 { + return + } + tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error(err) + return + } + if !tagConfig.RequiredTag { + for _, tag := range tagList { + tag.Recommend = false + } + } +} + +func (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, tag *entity.Tag) { + if tag == nil { + return + } + tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error(err) + return + } + if !tagConfig.RequiredTag { + tag.Recommend = false + } +} + // BatchGetObjectTag batch get object tag func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) { - objectIdTagMap := make(map[string][]*schema.TagResp) - tagIDList := make([]string, 0) - tagsInfoMap := make(map[string]*entity.Tag) - - tagList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds) + objectIDTagMap := make(map[string][]*schema.TagResp) + if len(objectIds) == 0 { + return objectIDTagMap, nil + } + objectTagRelList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds) if err != nil { - return objectIdTagMap, err + return objectIDTagMap, err } - for _, tag := range tagList { + tagIDList := make([]string, 0) + for _, tag := range objectTagRelList { tagIDList = append(tagIDList, tag.TagID) } - tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList) + tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList) if err != nil { - return objectIdTagMap, err + return objectIDTagMap, err } - for _, item := range tagsInfoList { - tagsInfoMap[item.ID] = item + tagsInfoMapping := make(map[string]*entity.Tag) + tagsRank := make(map[string]int) // Used for sorting + for idx, item := range tagsInfoList { + tagsInfoMapping[item.ID] = item + tagsRank[item.ID] = idx } - for _, item := range tagList { - _, ok := tagsInfoMap[item.TagID] + + for _, item := range objectTagRelList { + _, ok := tagsInfoMapping[item.TagID] if ok { - tagInfo := tagsInfoMap[item.TagID] + tagInfo := tagsInfoMapping[item.TagID] t := &schema.TagResp{ + ID: tagInfo.ID, SlugName: tagInfo.SlugName, DisplayName: tagInfo.DisplayName, MainTagSlugName: tagInfo.MainTagSlugName, + Recommend: tagInfo.Recommend, + Reserved: tagInfo.Reserved, } - objectIdTagMap[item.ObjectID] = append(objectIdTagMap[item.ObjectID], t) + objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t) + } + } + // The sorting in tagsRank is correct, object tags should be sorted by tagsRank + for _, objectTags := range objectIDTagMap { + sort.SliceStable(objectTags, func(i, j int) bool { + return tagsRank[objectTags[i].ID] < tagsRank[objectTags[j].ID] + }) + } + return objectIDTagMap, nil +} + +func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) { + if len(tags) == 0 { + return nil + } + + // find tags name + tagListInDb, err := ts.GetTagListByNames(ctx, tags) + if err != nil { + return err + } + + tagInDbMapping := make(map[string]*entity.Tag) + checktags := make([]string, 0) + + for _, tag := range tagListInDb { + if tag.MainTagID != 0 { + checktags = append(checktags, fmt.Sprintf("\"%s\"", tag.SlugName)) + } + tagInDbMapping[tag.SlugName] = tag + } + if len(checktags) > 0 { + err = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf("Should not contain synonym tags %s", strings.Join(checktags, ","))) + return err + } + + addTagList := make([]*entity.Tag, 0) + addTagMsgList := make([]string, 0) + for _, tag := range tags { + _, ok := tagInDbMapping[tag] + if ok { + continue } + item := &entity.Tag{} + item.SlugName = tag + item.DisplayName = tag + item.OriginalText = "" + item.ParsedText = "" + item.Status = entity.TagStatusAvailable + item.UserID = userID + addTagList = append(addTagList, item) + addTagMsgList = append(addTagMsgList, tag) + } + + if len(addTagList) > 0 { + err = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf("tag [%s] does not exist", + strings.Join(addTagMsgList, ","))) + return err + } - return objectIdTagMap, nil + + return nil +} + +// CheckTagsIsChange +func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList, oldtagNameList []string) bool { + check := make(map[string]bool) + if len(tagNameList) != len(oldtagNameList) { + return true + } + for _, item := range tagNameList { + check[item] = false + } + for _, item := range oldtagNameList { + _, ok := check[item] + if !ok { + return true + } + check[item] = true + } + for _, value := range check { + if !value { + return true + } + } + return false +} + +func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) { + reservedTagsMap := make(map[string]bool) + needTagsMap := make([]string, 0) + notNeedTagsMap := make([]string, 0) + for _, tag := range objectTagData { + if tag.Reserved { + reservedTagsMap[tag.SlugName] = true + } + } + for _, tag := range oldobjectTagData { + if tag.Reserved { + _, ok := reservedTagsMap[tag.SlugName] + if !ok { + needTagsMap = append(needTagsMap, tag.SlugName) + } else { + reservedTagsMap[tag.SlugName] = false + } + } + } + + for k, v := range reservedTagsMap { + if v { + notNeedTagsMap = append(notNeedTagsMap, k) + } + } + + if len(needTagsMap) > 0 { + return false, true, needTagsMap, []string{} + } + + if len(notNeedTagsMap) > 0 { + return true, false, []string{}, notNeedTagsMap + } + + return true, true, []string{}, []string{} } // ObjectChangeTag change object tag list @@ -144,34 +661,35 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * } // find tags name - tagListInDb, err := ts.tagRepo.GetTagListByNames(ctx, thisObjTagNameList) + tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList) if err != nil { return err } tagInDbMapping := make(map[string]*entity.Tag) for _, tag := range tagListInDb { - tagInDbMapping[tag.SlugName] = tag + tagInDbMapping[strings.ToLower(tag.SlugName)] = tag thisObjTagIDList = append(thisObjTagIDList, tag.ID) } addTagList := make([]*entity.Tag, 0) for _, tag := range objectTagData.Tags { - _, ok := tagInDbMapping[tag.SlugName] + _, ok := tagInDbMapping[strings.ToLower(tag.SlugName)] if ok { continue } item := &entity.Tag{} - item.SlugName = tag.SlugName + item.SlugName = strings.ReplaceAll(tag.SlugName, " ", "-") item.DisplayName = tag.DisplayName item.OriginalText = tag.OriginalText item.ParsedText = tag.ParsedText item.Status = entity.TagStatusAvailable + item.UserID = objectTagData.UserID addTagList = append(addTagList, item) } if len(addTagList) > 0 { - err = ts.tagRepo.AddTagList(ctx, addTagList) + err = ts.tagCommonRepo.AddTagList(ctx, addTagList) if err != nil { return err } @@ -184,20 +702,31 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * } tagInfoJson, _ := json.Marshal(tag) revisionDTO.Content = string(tagInfoJson) - err = ts.revisionService.AddRevision(ctx, revisionDTO, true) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return err } + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: objectTagData.UserID, + ObjectID: tag.ID, + OriginalObjectID: tag.ID, + ActivityTypeKey: constant.ActTagCreated, + RevisionID: revisionID, + }) } } - err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectId, thisObjTagIDList) + err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectID, thisObjTagIDList) if err != nil { return err } return nil } +func (ts *TagCommonService) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { + return ts.tagRelRepo.CountTagRelByTagID(ctx, tagID) +} + // RefreshTagQuestionCount refresh tag question count func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs []string) (err error) { for _, tagID := range tagIDs { @@ -205,7 +734,7 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs if err != nil { return err } - err = ts.tagRepo.UpdateTagQuestionCount(ctx, tagID, int(count)) + err = ts.tagCommonRepo.UpdateTagQuestionCount(ctx, tagID, int(count)) if err != nil { return err } @@ -214,12 +743,45 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs return nil } +func (ts *TagCommonService) RefreshTagCountByQuestionID(ctx context.Context, questionID string) (err error) { + tagListList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, questionID) + if err != nil { + return err + } + tagIDs := make([]string, 0) + for _, item := range tagListList { + tagIDs = append(tagIDs, item.TagID) + } + err = ts.RefreshTagQuestionCount(ctx, tagIDs) + if err != nil { + return err + } + return nil +} + +// RemoveTagRelListByObjectID remove tag relation by object id +func (ts *TagCommonService) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + return ts.tagRelRepo.RemoveTagRelListByObjectID(ctx, objectID) +} + +// RecoverTagRelListByObjectID recover tag relation by object id +func (ts *TagCommonService) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + return ts.tagRelRepo.RecoverTagRelListByObjectID(ctx, objectID) +} + +func (ts *TagCommonService) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + return ts.tagRelRepo.HideTagRelListByObjectID(ctx, objectID) +} + +func (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + return ts.tagRelRepo.ShowTagRelListByObjectID(ctx, objectID) +} + // CreateOrUpdateTagRelList if tag relation is exists update status, if not create it func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) { - addTagIDMapping := make(map[string]bool) - needRefreshTagIDs := make([]string, 0) + addTagIDMapping := make(map[string]struct{}) for _, t := range tagIDs { - addTagIDMapping[t] = true + addTagIDMapping[t] = struct{}{} } // get all old relation @@ -228,8 +790,10 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object return err } var deleteTagRel []int64 + needRefreshTagIDs := make([]string, 0, len(oldTagRelList)+len(tagIDs)) + needRefreshTagIDs = append(needRefreshTagIDs, tagIDs...) for _, rel := range oldTagRelList { - if !addTagIDMapping[rel.TagID] { + if _, ok := addTagIDMapping[rel.TagID]; !ok { deleteTagRel = append(deleteTagRel, rel.ID) needRefreshTagIDs = append(needRefreshTagIDs, rel.TagID) } @@ -237,8 +801,11 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object addTagRelList := make([]*entity.TagRel, 0) enableTagRelList := make([]int64, 0) + defaultTagRelStatus, err := ts.tagRelRepo.GetTagRelDefaultStatusByObjectID(ctx, objectId) + if err != nil { + return err + } for _, tagID := range tagIDs { - needRefreshTagIDs = append(needRefreshTagIDs, tagID) rel, exist, err := ts.tagRelRepo.GetObjectTagRelWithoutStatus(ctx, objectId, tagID) if err != nil { return err @@ -246,11 +813,11 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object // if not exist add tag relation if !exist { addTagRelList = append(addTagRelList, &entity.TagRel{ - TagID: tagID, ObjectID: objectId, Status: entity.TagStatusAvailable, + TagID: tagID, ObjectID: objectId, Status: defaultTagRelStatus, }) } // if exist and has been removed, that should be enabled - if exist && rel.Status != entity.TagStatusAvailable { + if exist && rel.Status != entity.TagRelStatusAvailable && rel.Status != entity.TagRelStatusHide { enableTagRelList = append(enableTagRelList, rel.ID) } } @@ -266,7 +833,7 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object } } if len(enableTagRelList) > 0 { - if err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList); err != nil { + if err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList, defaultTagRelStatus == entity.TagRelStatusHide); err != nil { return err } } @@ -277,3 +844,95 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object } return nil } + +func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (err error) { + var canUpdate bool + _, existUnreviewed, err := ts.revisionService.ExistUnreviewedByObjectID(ctx, req.TagID) + if err != nil { + return err + } + if existUnreviewed { + err = errors.BadRequest(reason.AnswerCannotUpdate) + return err + } + + tagInfo, exist, err := ts.GetTagByID(ctx, req.TagID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.TagNotFound) + } + + //Adding equivalent slug formatting for tag update + slugName := strings.ReplaceAll(req.SlugName, " ", "-") + slugName = strings.ToLower(slugName) + + //If the content is the same, ignore it + if tagInfo.OriginalText == req.OriginalText && + tagInfo.DisplayName == req.DisplayName && + tagInfo.SlugName == slugName { + return nil + } + + tagInfo.SlugName = slugName + tagInfo.DisplayName = req.DisplayName + tagInfo.OriginalText = req.OriginalText + tagInfo.ParsedText = req.ParsedText + + revisionDTO := &schema.AddRevisionDTO{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + Title: tagInfo.SlugName, + Log: req.EditSummary, + } + + if req.NoNeedReview { + canUpdate = true + err = ts.tagRepo.UpdateTag(ctx, tagInfo) + if err != nil { + return err + } + if tagInfo.MainTagID == 0 && len(req.SlugName) > 0 { + log.Debugf("tag %s update slug_name", tagInfo.SlugName) + tagList, err := ts.tagRepo.GetTagList(ctx, &entity.Tag{MainTagID: converter.StringToInt64(tagInfo.ID)}) + if err != nil { + return err + } + updateTagSlugNames := make([]string, 0) + for _, tag := range tagList { + updateTagSlugNames = append(updateTagSlugNames, tag.SlugName) + } + err = ts.tagRepo.UpdateTagSynonym(ctx, updateTagSlugNames, converter.StringToInt64(tagInfo.ID), tagInfo.MainTagSlugName) + if err != nil { + return err + } + } + revisionDTO.Status = entity.RevisionReviewPassStatus + } else { + revisionDTO.Status = entity.RevisionUnreviewedStatus + } + + tagInfoJson, _ := json.Marshal(tagInfo) + revisionDTO.Content = string(tagInfoJson) + revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true) + if err != nil { + return err + } + if canUpdate { + ts.activityQueueService.Send(ctx, &schema.ActivityMsg{ + UserID: req.UserID, + ObjectID: tagInfo.ID, + OriginalObjectID: tagInfo.ID, + ActivityTypeKey: constant.ActTagEdited, + RevisionID: revisionID, + }) + } + + return +} + +// MigrateTagQuestions migrate tag question +func (ts *TagCommonService) MigrateTagQuestions(ctx context.Context, sourceTagID, targetTagID string) (err error) { + return ts.tagRelRepo.MigrateTagObjects(ctx, sourceTagID, targetTagID) +} diff --git a/internal/service/unique/uniqid_service.go b/internal/service/unique/uniqid_service.go index a8bdc02c9..99c09f118 100644 --- a/internal/service/unique/uniqid_service.go +++ b/internal/service/unique/uniqid_service.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package unique import ( @@ -6,6 +25,5 @@ import ( // UniqueIDRepo unique id repository type UniqueIDRepo interface { - GenUniqueID(ctx context.Context, key string) (uniqueID int64, err error) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 029a8b6a4..2ae5369df 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -1,64 +1,398 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package uploader import ( + "bytes" "fmt" + "io" "mime/multipart" + "net/http" + "net/url" + "os" "path" "path/filepath" + "strings" + + "github.com/apache/answer/internal/service/file_record" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/service/service_config" - "github.com/answerdev/answer/pkg/dir" - "github.com/answerdev/answer/pkg/uid" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/service/service_config" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/dir" + "github.com/apache/answer/pkg/uid" + "github.com/apache/answer/plugin" + "github.com/disintegration/imaging" "github.com/gin-gonic/gin" + exifremove "github.com/scottleedavis/go-exif-remove" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) -const ( - avatarSubPath = "avatar" - postSubPath = "post" +var ( + subPathList = []string{ + constant.AvatarSubPath, + constant.AvatarThumbSubPath, + constant.PostSubPath, + constant.BrandingSubPath, + constant.FilesPostSubPath, + constant.DeletedSubPath, + } + supportedThumbFileExtMapping = map[string]imaging.Format{ + ".jpg": imaging.JPEG, + ".jpeg": imaging.JPEG, + ".png": imaging.PNG, + ".gif": imaging.GIF, + } ) -// UploaderService user service -type UploaderService struct { - serviceConfig *service_config.ServiceConfig +type UploaderService interface { + UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) + UploadPostFile(ctx *gin.Context, userID string) (url string, err error) + UploadPostAttachment(ctx *gin.Context, userID string) (url string, err error) + UploadBrandingFile(ctx *gin.Context, userID string) (url string, err error) + AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) +} + +// uploaderService uploader service +type uploaderService struct { + serviceConfig *service_config.ServiceConfig + siteInfoService siteinfo_common.SiteInfoCommonService + fileRecordService *file_record.FileRecordService } // NewUploaderService new upload service -func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderService { - err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) +func NewUploaderService( + serviceConfig *service_config.ServiceConfig, + siteInfoService siteinfo_common.SiteInfoCommonService, + fileRecordService *file_record.FileRecordService, +) UploaderService { + for _, subPath := range subPathList { + err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath)) + if err != nil { + panic(err) + } + } + return &uploaderService{ + serviceConfig: serviceConfig, + siteInfoService: siteInfoService, + fileRecordService: fileRecordService, + } +} + +// UploadAvatarFile upload avatar file +func (us *uploaderService) UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) { + url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar) + if err != nil { + return "", err + } + if len(url) > 0 { + return url, nil + } + + siteWrite, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } + + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize()) + file, fileHeader, err := ctx.Request.FormFile("file") + if err != nil { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + file.Close() + fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) + if _, ok := plugin.DefaultFileTypeCheckMapping[plugin.UserAvatar][fileExt]; !ok { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) + avatarFilePath := path.Join(constant.AvatarSubPath, newFilename) + url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) + if err != nil { + return "", err + } + us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserAvatar)) + return url, nil + +} + +func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) { + fileSuffix := path.Ext(fileName) + if _, ok := supportedThumbFileExtMapping[fileSuffix]; !ok { + // if file type is not supported, return original file + return path.Join(us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName), nil + } + if size > 1024 { + size = 1024 + } + + thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName) + thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarThumbSubPath, thumbFileName) + avatarFile, err := os.ReadFile(thumbFilePath) + if err == nil { + return thumbFilePath, nil + } + filePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName) + avatarFile, err = os.ReadFile(filePath) if err != nil { - panic(err) + return "", errors.NotFound(reason.UnknownError).WithError(err) } - err = dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, postSubPath)) + reader := bytes.NewReader(avatarFile) + img, err := imaging.Decode(reader) if err != nil { - panic(err) + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + + var buf bytes.Buffer + newImage := imaging.Fill(img, size, size, imaging.Center, imaging.Linear) + if err = imaging.Encode(&buf, newImage, supportedThumbFileExtMapping[fileSuffix]); err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + + if err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, constant.AvatarThumbSubPath)); err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + + avatarFilePath := path.Join(constant.AvatarThumbSubPath, thumbFileName) + saveFilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + out, err := os.Create(saveFilePath) + if err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + defer out.Close() + + thumbReader := bytes.NewReader(buf.Bytes()) + if _, err = io.Copy(out, thumbReader); err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return saveFilePath, nil +} + +func (us *uploaderService) UploadPostFile(ctx *gin.Context, userID string) ( + url string, err error) { + url, err = us.tryToUploadByPlugin(ctx, plugin.UserPost) + if err != nil { + return "", err + } + if len(url) > 0 { + return url, nil + } + + siteWrite, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } + + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize()) + file, fileHeader, err := ctx.Request.FormFile("file") + if err != nil { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) } - return &UploaderService{ - serviceConfig: serviceConfig, + defer file.Close() + if checker.IsUnAuthorizedExtension(fileHeader.Filename, siteWrite.AuthorizedImageExtensions) { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + + fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) + avatarFilePath := path.Join(constant.PostSubPath, newFilename) + url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) + if err != nil { + return "", err } + us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserPost)) + return url, nil } -func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( +func (us *uploaderService) UploadPostAttachment(ctx *gin.Context, userID string) ( url string, err error) { + url, err = us.tryToUploadByPlugin(ctx, plugin.UserPostAttachment) + if err != nil { + return "", err + } + if len(url) > 0 { + return url, nil + } + + resp, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } + + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, resp.GetMaxAttachmentSize()) + file, fileHeader, err := ctx.Request.FormFile("file") + if err != nil { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + defer file.Close() + if checker.IsUnAuthorizedExtension(fileHeader.Filename, resp.AuthorizedAttachmentExtensions) { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + + fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) - avatarFilePath := path.Join(avatarSubPath, newFilename) - return us.uploadFile(ctx, file, avatarFilePath) + attachmentFilePath := path.Join(constant.FilesPostSubPath, newFilename) + url, err = us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, attachmentFilePath) + if err != nil { + return "", err + } + us.fileRecordService.AddFileRecord(ctx, userID, attachmentFilePath, url, string(plugin.UserPostAttachment)) + return url, nil } -func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( +func (us *uploaderService) UploadBrandingFile(ctx *gin.Context, userID string) ( url string, err error) { + url, err = us.tryToUploadByPlugin(ctx, plugin.AdminBranding) + if err != nil { + return "", err + } + if len(url) > 0 { + return url, nil + } + + siteWrite, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } + + ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize()) + file, fileHeader, err := ctx.Request.FormFile("file") + if err != nil { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + file.Close() + fileExt := strings.ToLower(path.Ext(fileHeader.Filename)) + if _, ok := plugin.DefaultFileTypeCheckMapping[plugin.AdminBranding][fileExt]; !ok { + return "", errors.BadRequest(reason.RequestFormatError).WithError(err) + } + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) - avatarFilePath := path.Join(postSubPath, newFilename) - return us.uploadFile(ctx, file, avatarFilePath) + avatarFilePath := path.Join(constant.BrandingSubPath, newFilename) + url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath) + if err != nil { + return "", err + } + us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.AdminBranding)) + return url, nil + } -func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( +func (us *uploaderService) uploadImageFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( url string, err error) { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + return "", err + } + siteWrite, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, fileSubPath) + + src, err := file.Open() + if err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + defer src.Close() + + if !checker.DecodeAndCheckImageFile(filePath, siteWrite.GetMaxImageMegapixel()) { + return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat) + } + + if err := removeExif(filePath); err != nil { + log.Error(err) + } + + url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } + +func (us *uploaderService) uploadAttachmentFile(ctx *gin.Context, file *multipart.FileHeader, originalFilename, fileSubPath string) ( + downloadUrl string, err error) { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + return "", err + } + filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) + if err := ctx.SaveUploadedFile(file, filePath); err != nil { + return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + + // Need url encode the original filename. Because the filename may contain special characters that conflict with the markdown syntax. + originalFilename = url.QueryEscape(originalFilename) + + // The original filename is 123.pdf + // The local saved path is /UploadPath/hash.pdf + // When downloading, the download link will be redirect to the local saved path. And the download filename will be 123.png. + downloadPath := strings.TrimSuffix(fileSubPath, filepath.Ext(fileSubPath)) + "/" + originalFilename + downloadUrl = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, downloadPath) + return downloadUrl, nil +} + +func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) ( + url string, err error) { + siteWrite, err := us.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return "", err + } + cond := plugin.UploadFileCondition{ + Source: source, + MaxImageSize: siteWrite.MaxImageSize, + MaxAttachmentSize: siteWrite.MaxAttachmentSize, + MaxImageMegapixel: siteWrite.MaxImageMegapixel, + AuthorizedImageExtensions: siteWrite.AuthorizedImageExtensions, + AuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions, + } + _ = plugin.CallStorage(func(fn plugin.Storage) error { + resp := fn.UploadFile(ctx, cond) + if resp.OriginalError != nil { + log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError) + err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err) + } else { + url = resp.FullURL + } + return nil + }) + return url, err +} + +// removeExif remove exif +// only support jpg/jpeg/png +func removeExif(path string) error { + ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(path), ".")) + if ext != "jpeg" && ext != "jpg" && ext != "png" { + return nil + } + img, err := os.ReadFile(path) + if err != nil { + return err + } + noExifBytes, err := exifremove.Remove(img) + if err != nil { + return err + } + return os.WriteFile(path, noExifBytes, 0644) +} diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go new file mode 100644 index 000000000..0f6ec81ed --- /dev/null +++ b/internal/service/user_admin/user_backyard.go @@ -0,0 +1,664 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_admin + +import ( + "context" + "fmt" + "net/mail" + "strings" + "time" + "unicode" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/base/validator" + answercommon "github.com/apache/answer/internal/service/answer_common" + "github.com/apache/answer/internal/service/badge" + "github.com/apache/answer/internal/service/comment_common" + "github.com/apache/answer/internal/service/export" + notificationcommon "github.com/apache/answer/internal/service/notification_common" + "github.com/apache/answer/internal/service/plugin_common" + questioncommon "github.com/apache/answer/internal/service/question_common" + "github.com/apache/answer/pkg/token" + + "github.com/apache/answer/internal/base/pager" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/user_external_login" + "github.com/apache/answer/pkg/checker" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "golang.org/x/crypto/bcrypt" +) + +// UserAdminRepo user repository +type UserAdminRepo interface { + UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string, suspendedUntil time.Time) (err error) + GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) + GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) + GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, + usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) + AddUser(ctx context.Context, user *entity.User) (err error) + AddUsers(ctx context.Context, users []*entity.User) (err error) + UpdateUserPassword(ctx context.Context, userID string, password string) (err error) + DeletePermanentlyUsers(ctx context.Context) (err error) + GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) +} + +// UserAdminService user service +type UserAdminService struct { + userRepo UserAdminRepo + userRoleRelService *role.UserRoleRelService + authService *auth.AuthService + userCommonService *usercommon.UserCommon + userActivity activity.UserActiveActivityRepo + siteInfoCommonService siteinfo_common.SiteInfoCommonService + emailService *export.EmailService + questionCommonRepo questioncommon.QuestionRepo + answerCommonRepo answercommon.AnswerRepo + commentCommonRepo comment_common.CommentCommonRepo + userExternalLoginRepo user_external_login.UserExternalLoginRepo + notificationRepo notificationcommon.NotificationRepo + pluginUserConfigRepo plugin_common.PluginUserConfigRepo + badgeAwardRepo badge.BadgeAwardRepo +} + +// NewUserAdminService new user admin service +func NewUserAdminService( + userRepo UserAdminRepo, + userRoleRelService *role.UserRoleRelService, + authService *auth.AuthService, + userCommonService *usercommon.UserCommon, + userActivity activity.UserActiveActivityRepo, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + emailService *export.EmailService, + questionCommonRepo questioncommon.QuestionRepo, + answerCommonRepo answercommon.AnswerRepo, + commentCommonRepo comment_common.CommentCommonRepo, + userExternalLoginRepo user_external_login.UserExternalLoginRepo, + notificationRepo notificationcommon.NotificationRepo, + pluginUserConfigRepo plugin_common.PluginUserConfigRepo, + badgeAwardRepo badge.BadgeAwardRepo, +) *UserAdminService { + return &UserAdminService{ + userRepo: userRepo, + userRoleRelService: userRoleRelService, + authService: authService, + userCommonService: userCommonService, + userActivity: userActivity, + siteInfoCommonService: siteInfoCommonService, + emailService: emailService, + questionCommonRepo: questionCommonRepo, + answerCommonRepo: answerCommonRepo, + commentCommonRepo: commentCommonRepo, + userExternalLoginRepo: userExternalLoginRepo, + notificationRepo: notificationRepo, + pluginUserConfigRepo: pluginUserConfigRepo, + badgeAwardRepo: badgeAwardRepo, + } +} + +// UpdateUserStatus update user +func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { + // Admin cannot modify their status + if req.UserID == req.LoginUserID { + return errors.BadRequest(reason.AdminCannotModifySelfStatus) + } + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + // if user status is deleted + if userInfo.Status == entity.UserStatusDeleted { + return nil + } + + if req.IsInactive() { + userInfo.MailStatus = entity.EmailStatusToBeVerified + } + if req.IsDeleted() { + userInfo.Status = entity.UserStatusDeleted + userInfo.EMail = fmt.Sprintf("%s.%d", userInfo.EMail, time.Now().Unix()) + } + if req.IsSuspended() { + userInfo.Status = entity.UserStatusSuspended + } + if req.IsNormal() { + userInfo.Status = entity.UserStatusAvailable + userInfo.MailStatus = entity.EmailStatusAvailable + } + + suspendedUntil := req.GetSuspendedUntil() + err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail, suspendedUntil) + if err != nil { + return err + } + + // remove all content that user created, such as question, answer, comment, etc. + if req.RemoveAllContent { + us.removeAllUserCreatedContent(ctx, userInfo.ID) + } + + if req.IsDeleted() { + us.removeAllUserConfiguration(ctx, userInfo.ID) + } + + // if user reputation is zero means this user is inactive, so try to activate this user. + if req.IsNormal() && userInfo.Rank == 0 { + return us.userActivity.UserActive(ctx, userInfo.ID) + } + return nil +} + +// removeAllUserConfiguration remove all user configuration +func (us *UserAdminService) removeAllUserConfiguration(ctx context.Context, userID string) { + err := us.userExternalLoginRepo.DeleteUserExternalLoginByUserID(ctx, userID) + if err != nil { + log.Errorf("remove all user external login error: %v", err) + } + err = us.notificationRepo.DeleteNotification(ctx, userID) + if err != nil { + log.Errorf("remove all user notification error: %v", err) + } + err = us.notificationRepo.DeleteUserNotificationConfig(ctx, userID) + if err != nil { + log.Errorf("remove all user notification config error: %v", err) + } + err = us.pluginUserConfigRepo.DeleteUserPluginConfig(ctx, userID) + if err != nil { + log.Errorf("remove all user plugin config error: %v", err) + } + err = us.badgeAwardRepo.DeleteUserBadgeAward(ctx, userID) + if err != nil { + log.Errorf("remove all user badge award error: %v", err) + } +} + +// removeAllUserCreatedContent remove all user created content +func (us *UserAdminService) removeAllUserCreatedContent(ctx context.Context, userID string) { + if err := us.questionCommonRepo.RemoveAllUserQuestion(ctx, userID); err != nil { + log.Errorf("remove all user question error: %v", err) + } + if err := us.answerCommonRepo.RemoveAllUserAnswer(ctx, userID); err != nil { + log.Errorf("remove all user answer error: %v", err) + } + if err := us.commentCommonRepo.RemoveAllUserComment(ctx, userID); err != nil { + log.Errorf("remove all user comment error: %v", err) + } +} + +// UpdateUserRole update user role +func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) { + // Users cannot modify their roles + if req.UserID == req.LoginUserID { + return errors.BadRequest(reason.UserCannotUpdateYourRole) + } + + err = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID) + if err != nil { + return err + } + + us.authService.RemoveUserAllTokens(ctx, req.UserID) + return +} + +// AddUser add user +func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) { + _, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email) + if err != nil { + return err + } + if has { + return errors.BadRequest(reason.EmailDuplicate) + } + + hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + userInfo := &entity.User{} + userInfo.EMail = req.Email + userInfo.DisplayName = req.DisplayName + userInfo.Pass = string(hashPwd) + + userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) + if err != nil { + return err + } + userInfo.MailStatus = entity.EmailStatusAvailable + userInfo.Status = entity.UserStatusAvailable + userInfo.Rank = 1 + + err = us.userRepo.AddUser(ctx, userInfo) + if err != nil { + return err + } + return +} + +// AddUsers add users +func (us *UserAdminService) AddUsers(ctx context.Context, req *schema.AddUsersReq) ( + resp []*validator.FormErrorField, err error) { + resp, err = req.ParseUsers(ctx) + if err != nil { + return resp, err + } + errData := us.checkUserDuplicateInner(ctx, req.Users) + if errData != nil { + return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) + } + users, errData, err := us.formatBulkAddUsers(ctx, req) + if err != nil { + return resp, err + } + if errData != nil { + return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) + } + return nil, us.userRepo.AddUsers(ctx, users) +} + +func (us *UserAdminService) checkUserDuplicateInner(ctx context.Context, users []*schema.AddUserReq) ( + errorData *schema.AddUsersErrorData) { + lang := handler.GetLangByCtx(ctx) + val := validator.GetValidatorByLang(lang) + + emails := make(map[string]bool) + displayNames := make(map[string]bool) + for line, user := range users { + if errFields, e := val.Check(user); e != nil { + errorData = &schema.AddUsersErrorData{} + if len(errFields) > 0 { + errorData.Field = errFields[0].ErrorField + errorData.ExtraMessage = errFields[0].ErrorMsg + } + errorData.Line = line + 1 + errorData.Content = fmt.Sprintf("%s, %s, %s", user.DisplayName, user.Email, user.Password) + return errorData + } + if emails[user.Email] { + return &schema.AddUsersErrorData{ + Field: "email", + Line: line + 1, + Content: user.Email, + ExtraMessage: translator.Tr(lang, reason.EmailDuplicate), + } + } + if displayNames[user.DisplayName] { + return &schema.AddUsersErrorData{ + Field: "name", + Line: line + 1, + Content: user.DisplayName, + ExtraMessage: translator.Tr(lang, reason.UsernameDuplicate), + } + } + emails[user.Email] = true + displayNames[user.DisplayName] = true + } + return nil +} + +func (us *UserAdminService) formatBulkAddUsers(ctx context.Context, req *schema.AddUsersReq) ( + users []*entity.User, errorData *schema.AddUsersErrorData, err error) { + lang := handler.GetLangByCtx(ctx) + errorData = &schema.AddUsersErrorData{Line: -1} + for line, user := range req.Users { + _, has, e := us.userRepo.GetUserInfoByEmail(ctx, user.Email) + if e != nil { + return nil, nil, e + } + if has { + errorData.Field = "email" + errorData.Line = line + 1 + errorData.Content = user.Email + errorData.ExtraMessage = translator.Tr(lang, reason.EmailDuplicate) + return nil, errorData, nil + } + + userInfo := &entity.User{} + userInfo.EMail = user.Email + userInfo.DisplayName = user.DisplayName + hashPwd, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + userInfo.Pass = string(hashPwd) + userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) + if err != nil { + errorData.Field = "name" + errorData.Line = line + 1 + errorData.Content = user.DisplayName + errorData.ExtraMessage = translator.Tr(lang, reason.UsernameInvalid) + return nil, errorData, nil + } + userInfo.MailStatus = entity.EmailStatusAvailable + userInfo.Status = entity.UserStatusAvailable + userInfo.Rank = 1 + users = append(users, userInfo) + } + return users, nil, nil +} + +// UpdateUserPassword update user password +func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) { + // Users cannot modify their password + if req.UserID == req.LoginUserID { + return errors.BadRequest(reason.AdminCannotUpdateTheirPassword) + } + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + + hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + err = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd)) + if err != nil { + return err + } + // logout this user + us.authService.RemoveUserAllTokens(ctx, req.UserID) + return +} + +// EditUserProfile edit user profile +func (us *UserAdminService) EditUserProfile(ctx context.Context, req *schema.EditUserProfileReq) ( + errFields []*validator.FormErrorField, err error) { + if req.UserID == req.LoginUserID { + return nil, errors.BadRequest(reason.AdminCannotEditTheirProfile) + } + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + if checker.IsInvalidUsername(req.Username) || checker.IsUsersIgnorePath(req.Username) { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameInvalid, + }), errors.BadRequest(reason.UsernameInvalid) + } + + userInfo, exist, err = us.userCommonService.GetByUsername(ctx, req.Username) + if err != nil { + return nil, err + } + if exist && userInfo.ID != req.UserID { + return append(errFields, &validator.FormErrorField{ + ErrorField: "username", + ErrorMsg: reason.UsernameDuplicate, + }), errors.BadRequest(reason.UsernameDuplicate) + } + + userInfo, exist, err = us.userCommonService.GetByEmail(ctx, req.Email) + if err != nil { + return nil, err + } + if exist && userInfo.ID != req.UserID { + return append(errFields, &validator.FormErrorField{ + ErrorField: "email", + ErrorMsg: reason.EmailDuplicate, + }), errors.BadRequest(reason.EmailDuplicate) + } + + user := &entity.User{} + user.ID = req.UserID + user.DisplayName = req.DisplayName + user.Username = req.Username + user.EMail = req.Email + user.MailStatus = entity.EmailStatusAvailable + err = us.userCommonService.UpdateUserProfile(ctx, user) + if err != nil { + return nil, err + } + return +} + +// GetUserInfo get user one +func (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) { + user, exist, err := us.userRepo.GetUserInfo(ctx, userID) + if err != nil { + return + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + resp = &schema.GetUserInfoResp{} + _ = copier.Copy(resp, user) + return resp, nil +} + +// GetUserPage get user list page +func (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) { + user := &entity.User{} + _ = copier.Copy(user, req) + + if req.IsInactive() { + user.MailStatus = entity.EmailStatusToBeVerified + user.Status = entity.UserStatusAvailable + } else if req.IsSuspended() { + user.Status = entity.UserStatusSuspended + } else if req.IsDeleted() { + user.Status = entity.UserStatusDeleted + } else { + user.MailStatus = entity.EmailStatusAvailable + user.Status = entity.UserStatusAvailable + } + + if len(req.Query) > 0 { + if email, e := mail.ParseAddress(req.Query); e == nil { + user.EMail = email.Address + req.Query = "" + } else if strings.HasPrefix(req.Query, "user:") { + id := strings.TrimSpace(strings.TrimPrefix(req.Query, "user:")) + idSearch := true + for _, r := range id { + if !unicode.IsDigit(r) { + idSearch = false + break + } + } + if idSearch { + user.ID = id + req.Query = "" + } else { + req.Query = id + } + } + } + + users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query, req.Staff) + if err != nil { + return + } + avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, users) + + resp := make([]*schema.GetUserPageResp, 0) + for _, u := range users { + t := &schema.GetUserPageResp{ + UserID: u.ID, + CreatedAt: u.CreatedAt.Unix(), + Username: u.Username, + EMail: u.EMail, + Rank: u.Rank, + DisplayName: u.DisplayName, + Avatar: avatarMapping[u.ID].GetURL(), + } + if u.Status == entity.UserStatusDeleted { + t.Status = constant.UserDeleted + t.DeletedAt = u.DeletedAt.Unix() + } else if u.Status == entity.UserStatusSuspended { + t.Status = constant.UserSuspended + t.SuspendedAt = u.SuspendedAt.Unix() + if !u.SuspendedUntil.IsZero() { + t.SuspendedUntil = u.SuspendedUntil.Unix() + } + } else if u.MailStatus == entity.EmailStatusToBeVerified { + t.Status = constant.UserInactive + } else { + t.Status = constant.UserNormal + } + resp = append(resp, t) + } + us.setUserRoleInfo(ctx, resp) + return pager.NewPageModel(total, resp), nil +} + +func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) { + var userIDs []string + for _, u := range resp { + userIDs = append(userIDs, u.UserID) + } + + userRoleMapping, err := us.userRoleRelService.GetUserRoleMapping(ctx, userIDs) + if err != nil { + log.Error(err) + return + } + + for _, u := range resp { + r := userRoleMapping[u.UserID] + if r == nil { + continue + } + u.RoleID = r.ID + u.RoleName = r.Name + } +} + +func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) ( + resp *schema.GetUserActivationResp, err error) { + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + + general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + return nil, err + } + + data := &schema.EmailCodeContent{ + Email: userInfo.EMail, + UserID: userInfo.ID, + } + code := token.GenerateToken() + us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString()) + resp = &schema.GetUserActivationResp{ + ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code), + } + return resp, nil +} + +// SendUserActivation send user activation email +func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) { + userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) + if err != nil { + return err + } + if !exist { + return errors.BadRequest(reason.UserNotFound) + } + + general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + return err + } + + data := &schema.EmailCodeContent{ + Email: userInfo.EMail, + UserID: userInfo.ID, + } + code := token.GenerateToken() + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) + if err != nil { + return err + } + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) + return nil +} + +func (us *UserAdminService) DeletePermanently(ctx context.Context, req *schema.DeletePermanentlyReq) (err error) { + if req.Type == constant.DeletePermanentlyUsers { + return us.userRepo.DeletePermanentlyUsers(ctx) + } else if req.Type == constant.DeletePermanentlyQuestions { + return us.questionCommonRepo.DeletePermanentlyQuestions(ctx) + } else if req.Type == constant.DeletePermanentlyAnswers { + return us.answerCommonRepo.DeletePermanentlyAnswers(ctx) + } + + return errors.BadRequest(reason.RequestFormatError) +} + +// CheckAndUnsuspendExpiredUsers checks for users whose suspension has expired and restores them to normal status +func (us *UserAdminService) CheckAndUnsuspendExpiredUsers(ctx context.Context) error { + // Find all suspended users whose suspension time has expired + expiredUsers, err := us.userRepo.GetExpiredSuspendedUsers(ctx) + if err != nil { + return err + } + + now := time.Now() + for _, user := range expiredUsers { + // Check if suspension has expired (not permanent and time has passed) + if user.Status == entity.UserStatusSuspended && + !user.SuspendedUntil.IsZero() && + user.SuspendedUntil.Before(now) { + + log.Infof("Unsuspending user %s (ID: %s) - suspension expired at %v", + user.Username, user.ID, user.SuspendedUntil) + + // Update user status to normal + err = us.userRepo.UpdateUserStatus(ctx, user.ID, entity.UserStatusAvailable, + entity.EmailStatusAvailable, user.EMail, time.Time{}) + if err != nil { + log.Errorf("Failed to unsuspend user %s (ID: %s): %v", + user.Username, user.ID, err) + continue + } + } + } + + return nil +} diff --git a/internal/service/user_backyard/user_backyard.go b/internal/service/user_backyard/user_backyard.go deleted file mode 100644 index c82ce1ae2..000000000 --- a/internal/service/user_backyard/user_backyard.go +++ /dev/null @@ -1,124 +0,0 @@ -package user_backyard - -import ( - "context" - "fmt" - "time" - - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" -) - -// UserBackyardRepo user repository -type UserBackyardRepo interface { - UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string) (err error) - GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) - GetUserPage(ctx context.Context, page, pageSize int, user *entity.User) (users []*entity.User, total int64, err error) -} - -// UserBackyardService user service -type UserBackyardService struct { - userRepo UserBackyardRepo -} - -func NewUserBackyardService(userRepo UserBackyardRepo) *UserBackyardService { - return &UserBackyardService{ - userRepo: userRepo, - } -} - -// UpdateUserStatus update user -func (us *UserBackyardService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { - userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) - if err != nil { - return - } - if !exist { - return errors.BadRequest(reason.UserNotFound) - } - // if user status is deleted - if userInfo.Status == entity.UserStatusDeleted { - return nil - } - - if req.IsInactive() { - userInfo.MailStatus = entity.EmailStatusToBeVerified - } - if req.IsDeleted() { - userInfo.Status = entity.UserStatusDeleted - userInfo.EMail = fmt.Sprintf("%s.%d", userInfo.EMail, time.Now().UnixNano()) - } - if req.IsSuspended() { - userInfo.Status = entity.UserStatusSuspended - } - if req.IsNormal() { - userInfo.Status = entity.UserStatusAvailable - userInfo.MailStatus = entity.EmailStatusAvailable - } - return us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail) -} - -// GetUserInfo get user one -func (us *UserBackyardService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) { - user, exist, err := us.userRepo.GetUserInfo(ctx, userID) - if err != nil { - return - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - - resp = &schema.GetUserInfoResp{} - _ = copier.Copy(resp, user) - return resp, nil -} - -// GetUserPage get user list page -func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) { - user := &entity.User{} - _ = copier.Copy(user, req) - - if req.IsInactive() { - user.MailStatus = entity.EmailStatusToBeVerified - user.Status = entity.UserStatusAvailable - } else if req.IsSuspended() { - user.Status = entity.UserStatusSuspended - } else if req.IsDeleted() { - user.Status = entity.UserStatusDeleted - } - - users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user) - if err != nil { - return - } - - resp := make([]*schema.GetUserPageResp, 0) - for _, u := range users { - t := &schema.GetUserPageResp{ - UserID: u.ID, - CreatedAt: u.CreatedAt.Unix(), - Username: u.Username, - EMail: u.EMail, - Rank: u.Rank, - DisplayName: u.DisplayName, - Avatar: u.Avatar, - } - if u.Status == entity.UserStatusDeleted { - t.Status = schema.UserDeleted - t.DeletedAt = u.DeletedAt.Unix() - } else if u.Status == entity.UserStatusSuspended { - t.Status = schema.UserSuspended - t.SuspendedAt = u.SuspendedAt.Unix() - } else if u.MailStatus == entity.EmailStatusToBeVerified { - t.Status = schema.UserInactive - } else { - t.Status = schema.UserNormal - } - resp = append(resp, t) - } - return pager.NewPageModel(total, resp), nil -} diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 0297ae477..124972a16 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -1,45 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package usercommon import ( "context" + "strings" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/pkg/converter" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/auth" + "github.com/apache/answer/internal/service/role" + "github.com/apache/answer/internal/service/siteinfo_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/random" + "github.com/mozillazg/go-pinyin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) + UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) + UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) UpdateLastLoginDate(ctx context.Context, userID string) (err error) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateEmail(ctx context.Context, userID, email string) error - UpdatePass(ctx context.Context, Data *entity.User) error + UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) + UpdatePass(ctx context.Context, userID, pass string) error UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) + UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) + GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) + GetUserCount(ctx context.Context) (count int64, err error) + SearchUserListByName(ctx context.Context, name string, limit int, onlyStaff bool) (userList []*entity.User, err error) + IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) } // UserCommon user service type UserCommon struct { - userRepo UserRepo + userRepo UserRepo + userRoleService *role.UserRoleRelService + authService *auth.AuthService + siteInfoCommonService siteinfo_common.SiteInfoCommonService } -func NewUserCommon(userRepo UserRepo) *UserCommon { +func NewUserCommon( + userRepo UserRepo, + userRoleService *role.UserRoleRelService, + authService *auth.AuthService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, +) *UserCommon { return &UserCommon{ - userRepo: userRepo, + userRepo: userRepo, + userRoleService: userRoleService, + authService: authService, + siteInfoCommonService: siteInfoCommonService, } } -func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) (*schema.UserBasicInfo, bool, error) { +func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) ( + userBasicInfo *schema.UserBasicInfo, exist bool, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, ID) if err != nil { return nil, exist, err } - info := us.UserBasicInfoFormat(ctx, userInfo) + info := us.FormatUserBasicInfo(ctx, userInfo) + info.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() return info, exist, nil } @@ -48,46 +101,161 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s if err != nil { return nil, exist, err } - info := us.UserBasicInfoFormat(ctx, userInfo) + info := us.FormatUserBasicInfo(ctx, userInfo) + info.Avatar = us.siteInfoCommonService.FormatAvatar(ctx, userInfo.Avatar, userInfo.EMail, userInfo.Status).GetURL() return info, exist, nil } +func (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) { + infomap := make(map[string]*schema.UserBasicInfo) + list, err := us.userRepo.GetByUsernames(ctx, usernames) + if err != nil { + return infomap, err + } + avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, list) + for _, user := range list { + info := us.FormatUserBasicInfo(ctx, user) + info.Avatar = avatarMapping[user.ID].GetURL() + infomap[user.Username] = info + } + return infomap, nil +} + +func (us *UserCommon) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) { + return us.userRepo.GetByEmail(ctx, email) +} + +func (us *UserCommon) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) { + return us.userRepo.GetByUsername(ctx, username) +} + +func (us *UserCommon) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) { + return us.userRepo.UpdateUserProfile(ctx, userInfo) +} + func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error { - return us.userRepo.IncreaseAnswerCount(ctx, userID, num) + return us.userRepo.UpdateAnswerCount(ctx, userID, num) } -func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int) error { - return us.userRepo.IncreaseQuestionCount(ctx, userID, num) +func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int64) error { + return us.userRepo.UpdateQuestionCount(ctx, userID, num) } -func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) (map[string]*schema.UserBasicInfo, error) { +func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, userIDs []string) (map[string]*schema.UserBasicInfo, error) { + userIDs = checker.FilterEmptyString(userIDs) userMap := make(map[string]*schema.UserBasicInfo) - dbInfo, err := us.userRepo.BatchGetByID(ctx, IDs) + if len(userIDs) == 0 { + return userMap, nil + } + userList, err := us.userRepo.BatchGetByID(ctx, userIDs) if err != nil { return userMap, err } - for _, item := range dbInfo { - info := us.UserBasicInfoFormat(ctx, item) - userMap[item.ID] = info + avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, userList) + for _, user := range userList { + info := us.FormatUserBasicInfo(ctx, user) + info.Avatar = avatarMapping[user.ID].GetURL() + userMap[user.ID] = info + } + for _, id := range userIDs { + if _, ok := userMap[id]; !ok { + userMap[id] = &schema.UserBasicInfo{ + ID: id, + DisplayName: "user" + converter.DeleteUserDisplay(id), + Status: constant.UserDeleted, + } + } } return userMap, nil } -// UserBasicInfoFormat -func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { +// FormatUserBasicInfo format user basic info +func (us *UserCommon) FormatUserBasicInfo(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { userBasicInfo := &schema.UserBasicInfo{} userBasicInfo.ID = userInfo.ID userBasicInfo.Username = userInfo.Username userBasicInfo.Rank = userInfo.Rank userBasicInfo.DisplayName = userInfo.DisplayName - userBasicInfo.Avatar = userInfo.Avatar userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location - userBasicInfo.IpInfo = userInfo.IPInfo - userBasicInfo.Status = schema.UserStatusShow[userInfo.Status] - if userBasicInfo.Status == schema.UserDeleted { + userBasicInfo.Language = userInfo.Language + userBasicInfo.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus) + if !userInfo.SuspendedUntil.IsZero() { + userBasicInfo.SuspendedUntil = userInfo.SuspendedUntil.Unix() + } + if userBasicInfo.Status == constant.UserDeleted { userBasicInfo.Avatar = "" - userBasicInfo.DisplayName = "Anonymous" + userBasicInfo.DisplayName = "user" + converter.DeleteUserDisplay(userInfo.ID) } return userBasicInfo } + +// MakeUsername +// Generate a unique Username based on the displayName +func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (username string, err error) { + // Chinese processing + if has := checker.IsChinese(displayName); has { + displayName = strings.Join(pinyin.LazyConvert(displayName, nil), "") + } + + username = strings.ReplaceAll(displayName, " ", "-") + username = strings.ToLower(username) + suffix := "" + + if checker.IsInvalidUsername(username) { + return "", errors.BadRequest(reason.UsernameInvalid) + } + + if checker.IsReservedUsername(username) { + return "", errors.BadRequest(reason.UsernameInvalid) + } + + for { + _, has, err := us.userRepo.GetByUsername(ctx, username+suffix) + if err != nil { + return "", err + } + if !has { + break + } + suffix = random.UsernameSuffix() + } + return username + suffix, nil +} + +func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) ( + accessToken string, userCacheInfo *entity.UserCacheInfo, err error) { + roleID, err := us.userRoleService.GetUserRole(ctx, userID) + if err != nil { + log.Error(err) + } + + userCacheInfo = &entity.UserCacheInfo{ + UserID: userID, + EmailStatus: emailStatus, + UserStatus: userStatus, + RoleID: roleID, + ExternalID: externalID, + } + + accessToken, _, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) + if err != nil { + return "", nil, err + } + if userCacheInfo.RoleID == role.RoleAdminID { + if err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, userCacheInfo); err != nil { + return "", nil, err + } + } + return accessToken, userCacheInfo, nil +} + +func (us *UserCommon) IsAvatarFileUsed(ctx context.Context, filePath string) bool { + used, err := us.userRepo.IsAvatarFileUsed(ctx, filePath) + if err != nil { + log.Errorf("error checking if branding file is used: %v", err) + // will try again with the next clean up + return true + } + return used +} diff --git a/internal/service/user_external_login/user_center_login_service.go b/internal/service/user_external_login/user_center_login_service.go new file mode 100644 index 000000000..90895ac4a --- /dev/null +++ b/internal/service/user_external_login/user_center_login_service.go @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_external_login + +import ( + "context" + "encoding/json" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/converter" + "github.com/apache/answer/pkg/random" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" +) + +// UserCenterLoginService user external login service +type UserCenterLoginService struct { + userRepo usercommon.UserRepo + userExternalLoginRepo UserExternalLoginRepo + userCommonService *usercommon.UserCommon + userActivity activity.UserActiveActivityRepo + siteInfoCommonService siteinfo_common.SiteInfoCommonService +} + +// NewUserCenterLoginService new user external login service +func NewUserCenterLoginService( + userRepo usercommon.UserRepo, + userCommonService *usercommon.UserCommon, + userExternalLoginRepo UserExternalLoginRepo, + userActivity activity.UserActiveActivityRepo, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, +) *UserCenterLoginService { + return &UserCenterLoginService{ + userRepo: userRepo, + userCommonService: userCommonService, + userExternalLoginRepo: userExternalLoginRepo, + userActivity: userActivity, + siteInfoCommonService: siteInfoCommonService, + } +} + +func (us *UserCenterLoginService) ExternalLogin( + ctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) ( + resp *schema.UserExternalLoginResp, err error) { + if len(basicUserInfo.ExternalID) == 0 { + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID), + }, nil + } + + if len(basicUserInfo.Email) > 0 { + // check whether site allow register or not + siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + return nil, err + } + if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) { + log.Debugf("email domain not allowed: %s", basicUserInfo.Email) + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError), + }, nil + } + } + + oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, + userCenter.Info().SlugName, basicUserInfo.ExternalID) + if err != nil { + return nil, err + } + if exist { + // if user is already a member, login directly + oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID) + if err != nil { + return nil, err + } + if exist { + // if user is deleted, do not allow login + if oldUserInfo.Status == entity.UserStatusDeleted { + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied), + }, nil + } + if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil { + log.Errorf("update user last login date failed: %v", err) + } + accessToken, _, err := us.userCommonService.CacheLoginUserInfo( + ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) + return &schema.UserExternalLoginResp{AccessToken: accessToken}, err + } + } + + // cache external user info, waiting for user enter email address. + if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 { + return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil + } + + oldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo) + if err != nil { + return nil, err + } + + if err := us.activeUser(ctx, oldUserInfo); err != nil { + return nil, err + } + + accessToken, _, err := us.userCommonService.CacheLoginUserInfo( + ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) + return &schema.UserExternalLoginResp{AccessToken: accessToken}, err +} + +func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string, + basicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) { + userInfo = &entity.User{} + userInfo.EMail = basicUserInfo.Email + userInfo.DisplayName = basicUserInfo.DisplayName + + userInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username) + if err != nil { + log.Error(err) + userInfo.Username = random.Username() + } + + if len(basicUserInfo.Avatar) > 0 { + avatarInfo := &schema.AvatarInfo{ + Type: constant.AvatarTypeCustom, + Custom: basicUserInfo.Avatar, + } + avatar, _ := json.Marshal(avatarInfo) + userInfo.Avatar = string(avatar) + } + + userInfo.MailStatus = entity.EmailStatusAvailable + userInfo.Status = entity.UserStatusAvailable + userInfo.LastLoginDate = time.Now() + userInfo.Bio = basicUserInfo.Bio + userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio) + err = us.userRepo.AddUser(ctx, userInfo) + if err != nil { + return nil, err + } + + metaInfo, _ := json.Marshal(basicUserInfo) + newExternalUserInfo := &entity.UserExternalLogin{ + UserID: userInfo.ID, + Provider: provider, + ExternalID: basicUserInfo.ExternalID, + MetaInfo: string(metaInfo), + } + err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo) + if err != nil { + return nil, err + } + return userInfo, nil +} + +func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) error { + if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil { + log.Error(err) + return err + } + return nil +} + +func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) ( + resp *schema.UserCenterUserSettingsResp, err error) { + resp = &schema.UserCenterUserSettingsResp{} + + userCenter, ok := plugin.GetUserCenter() + if !ok { + return resp, nil + } + + // get external login info + externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID) + if err != nil { + return nil, err + } + var externalInfo *entity.UserExternalLogin + for _, t := range externalLoginList { + if t.Provider == userCenter.Info().SlugName { + externalInfo = t + } + } + if externalInfo == nil { + return resp, nil + } + + settings, err := userCenter.UserSettings(externalInfo.ExternalID) + if err != nil { + log.Error(err) + return resp, nil + } + + if len(settings.AccountSettingRedirectURL) > 0 { + resp.AccountSettingAgent = schema.UserSettingAgent{ + Enabled: true, + RedirectURL: settings.AccountSettingRedirectURL, + } + } + if len(settings.ProfileSettingRedirectURL) > 0 { + resp.ProfileSettingAgent = schema.UserSettingAgent{ + Enabled: true, + RedirectURL: settings.ProfileSettingRedirectURL, + } + } + return resp, nil +} + +// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions +// are turned off due to turning on the User Center plugin. +func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) ( + resp *schema.UserCenterAdminFunctionAgentResp, err error) { + resp = &schema.UserCenterAdminFunctionAgentResp{ + AllowCreateUser: true, + AllowUpdateUserStatus: true, + AllowUpdateUserPassword: true, + AllowUpdateUserRole: true, + } + userCenter, ok := plugin.GetUserCenter() + if !ok { + return + } + desc := userCenter.Description() + // If user status agent is enabled, admin can not update user status in answer. + resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled + resp.AllowUpdateUserRole = !desc.UserRoleAgentEnabled + + // If original user system is enabled, admin can update user password and role in answer. + resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem + resp.AllowCreateUser = desc.EnabledOriginalUserSystem + return resp, nil +} + +func (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) ( + resp *schema.UserCenterPersonalBranding, err error) { + resp = &schema.UserCenterPersonalBranding{ + PersonalBranding: make([]*schema.PersonalBranding, 0), + } + userCenter, ok := plugin.GetUserCenter() + if !ok { + return + } + + userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) + if err != nil { + return nil, err + } + if !exist { + return resp, nil + } + + // get external login info + externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID) + if err != nil { + return nil, err + } + var externalInfo *entity.UserExternalLogin + for _, t := range externalLoginList { + if t.Provider == userCenter.Info().SlugName { + externalInfo = t + } + } + if externalInfo == nil { + return resp, nil + } + + resp.Enabled = true + branding := userCenter.PersonalBranding(externalInfo.ExternalID) + + for _, t := range branding { + resp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{ + Icon: t.Icon, + Name: t.Name, + Label: t.Label, + Url: t.Url, + }) + } + return resp, nil +} diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go new file mode 100644 index 000000000..3f82179b7 --- /dev/null +++ b/internal/service/user_external_login/user_external_login_service.go @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_external_login + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/internal/base/translator" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/export" + "github.com/apache/answer/internal/service/siteinfo_common" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/apache/answer/internal/service/user_notification_config" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/random" + "github.com/apache/answer/pkg/token" + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +type UserExternalLoginRepo interface { + AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) + UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) + GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error) + GetByUserID(ctx context.Context, provider, userID string) (userInfo *entity.UserExternalLogin, exist bool, err error) + GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) + DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) + DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) + SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) + GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) +} + +// UserExternalLoginService user external login service +type UserExternalLoginService struct { + userRepo usercommon.UserRepo + userExternalLoginRepo UserExternalLoginRepo + userCommonService *usercommon.UserCommon + emailService *export.EmailService + siteInfoCommonService siteinfo_common.SiteInfoCommonService + userActivity activity.UserActiveActivityRepo + userNotificationConfigService *user_notification_config.UserNotificationConfigService +} + +// NewUserExternalLoginService new user external login service +func NewUserExternalLoginService( + userRepo usercommon.UserRepo, + userCommonService *usercommon.UserCommon, + userExternalLoginRepo UserExternalLoginRepo, + emailService *export.EmailService, + siteInfoCommonService siteinfo_common.SiteInfoCommonService, + userActivity activity.UserActiveActivityRepo, + userNotificationConfigService *user_notification_config.UserNotificationConfigService, +) *UserExternalLoginService { + return &UserExternalLoginService{ + userRepo: userRepo, + userCommonService: userCommonService, + userExternalLoginRepo: userExternalLoginRepo, + emailService: emailService, + siteInfoCommonService: siteInfoCommonService, + userActivity: userActivity, + userNotificationConfigService: userNotificationConfigService, + } +} + +// ExternalLogin if user is already a member logged in +func (us *UserExternalLoginService) ExternalLogin( + ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) ( + resp *schema.UserExternalLoginResp, err error) { + if len(externalUserInfo.ExternalID) == 0 { + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserExternalLoginMissingUserID), + }, nil + } + + oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, + externalUserInfo.Provider, externalUserInfo.ExternalID) + if err != nil { + return nil, err + } + if exist { + // if user is already a member, login directly + oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID) + if err != nil { + return nil, err + } + if exist && oldUserInfo.Status != entity.UserStatusDeleted { + if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil { + log.Errorf("update user last login date failed: %v", err) + } + newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo) + if err != nil { + log.Error(err) + } + accessToken, _, err := us.userCommonService.CacheLoginUserInfo( + ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) + return &schema.UserExternalLoginResp{AccessToken: accessToken}, err + } + } + + // cache external user info, waiting for user enter email address. + if len(externalUserInfo.Email) == 0 { + bindingKey := token.GenerateToken() + err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo) + if err != nil { + return nil, err + } + return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil + } + + // check whether site allow register or not + siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx) + if err != nil { + return nil, err + } + if !checker.EmailInAllowEmailDomain(externalUserInfo.Email, siteInfo.AllowEmailDomains) { + log.Debugf("email domain not allowed: %s", externalUserInfo.Email) + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError), + }, nil + } + + oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email) + if err != nil { + return nil, err + } + // if user is not a member, register a new user + if !exist { + oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo) + if err != nil { + return nil, err + } + } + // bind external user info to user + err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo) + if err != nil { + return nil, err + } + + // If user login with external account and email is exist, active user directly. + newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo) + if err != nil { + log.Error(err) + } + + // set default user notification config for external user + if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{oldUserInfo.ID}); err != nil { + log.Errorf("set default user notification config failed, err: %v", err) + } + + accessToken, _, err := us.userCommonService.CacheLoginUserInfo( + ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) + return &schema.UserExternalLoginResp{AccessToken: accessToken}, err +} + +func (us *UserExternalLoginService) registerNewUser(ctx context.Context, + externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) { + userInfo = &entity.User{} + userInfo.EMail = externalUserInfo.Email + userInfo.DisplayName = externalUserInfo.DisplayName + + userInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username) + if err != nil { + log.Error(err) + userInfo.Username = random.Username() + } + + if len(externalUserInfo.Avatar) > 0 { + avatarInfo := &schema.AvatarInfo{ + Type: constant.AvatarTypeCustom, + Custom: externalUserInfo.Avatar, + } + avatar, _ := json.Marshal(avatarInfo) + userInfo.Avatar = string(avatar) + } + + userInfo.MailStatus = entity.EmailStatusToBeVerified + userInfo.Status = entity.UserStatusAvailable + userInfo.LastLoginDate = time.Now() + userInfo.Bio = externalUserInfo.Bio + userInfo.BioHTML = externalUserInfo.Bio + err = us.userRepo.AddUser(ctx, userInfo) + if err != nil { + return nil, err + } + return userInfo, nil +} + +func (us *UserExternalLoginService) bindOldUser(ctx context.Context, + externalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) { + oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, + externalUserInfo.Provider, + externalUserInfo.ExternalID) + if err != nil { + return err + } + if exist { + oldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo + oldExternalUserInfo.UserID = oldUserInfo.ID + err = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo) + } else { + newExternalUserInfo := &entity.UserExternalLogin{ + UserID: oldUserInfo.ID, + Provider: externalUserInfo.Provider, + ExternalID: externalUserInfo.ExternalID, + MetaInfo: externalUserInfo.MetaInfo, + } + err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo) + } + return err +} + +func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User, + externalUserInfo *schema.ExternalLoginUserInfoCache) ( + mailStatus int, err error) { + log.Infof("user %s login with external account, try to active email, old status is %d", + oldUserInfo.ID, oldUserInfo.MailStatus) + + // try to active user email + if oldUserInfo.MailStatus == entity.EmailStatusToBeVerified { + err = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable) + if err != nil { + return oldUserInfo.MailStatus, err + } + } + + // try to update user avatar + if oldUserInfo.Avatar == "" && len(externalUserInfo.Avatar) > 0 { + avatarInfo := &schema.AvatarInfo{ + Type: constant.AvatarTypeCustom, + Custom: externalUserInfo.Avatar, + } + avatar, _ := json.Marshal(avatarInfo) + oldUserInfo.Avatar = string(avatar) + err = us.userRepo.UpdateInfo(ctx, oldUserInfo) + if err != nil { + log.Error(err) + } + } + + if err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil { + return oldUserInfo.MailStatus, err + } + return entity.EmailStatusAvailable, nil +} + +// ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user +func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail( + ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) ( + resp *schema.ExternalLoginBindingUserSendEmailResp, err error) { + siteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx) + if err != nil { + return nil, err + } + resp = &schema.ExternalLoginBindingUserSendEmailResp{} + externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey) + if err != nil || externalLoginInfo == nil { + return nil, errors.BadRequest(reason.UserNotFound) + } + if len(externalLoginInfo.Email) > 0 { + log.Warnf("the binding email has been sent %s", req.BindingKey) + return &schema.ExternalLoginBindingUserSendEmailResp{}, nil + } + + userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) + if err != nil { + return nil, err + } + if exist && !req.Must { + resp.EmailExistAndMustBeConfirmed = true + return resp, nil + } + + if !exist { + externalLoginInfo.Email = req.Email + userInfo, err = us.registerNewUser(ctx, externalLoginInfo) + if err != nil { + return nil, err + } + resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo( + ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID) + if err != nil { + log.Error(err) + } + } + err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo) + if err != nil { + return nil, err + } + + // send bind confirmation email + data := &schema.EmailCodeContent{ + SourceType: schema.BindingSourceType, + Email: req.Email, + UserID: userInfo.ID, + BindingKey: req.BindingKey, + } + code := token.GenerateToken() + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", siteGeneral.SiteUrl, code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) + if err != nil { + return nil, err + } + go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) + return resp, nil +} + +// ExternalLoginBindingUser +// The user clicks on the email link of the bound account and requests the API to bind the user officially +func (us *UserExternalLoginService) ExternalLoginBindingUser( + ctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) { + externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey) + if err != nil || externalLoginInfo == nil { + return errors.BadRequest(reason.UserNotFound) + } + return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo) +} + +// GetExternalLoginUserInfoList get external login user info list +func (us *UserExternalLoginService) GetExternalLoginUserInfoList( + ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) { + return us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID) +} + +// ExternalLoginUnbinding external login unbinding +func (us *UserExternalLoginService) ExternalLoginUnbinding( + ctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) { + // If user has only one external login and never set password, he can't unbind it. + userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.UserNotFound) + } + if len(userInfo.Pass) == 0 { + loginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID) + if err != nil { + return nil, err + } + if len(loginList) <= 1 { + return schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden) + } + } + + return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID) +} + +// CheckUserStatusInUserCenter check user status in user center +func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) ( + valid bool, externalID string, err error) { + // If enable user center plugin, user status should be checked by user center + userCenter, ok := plugin.GetUserCenter() + if !ok { + return true, "", nil + } + userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID) + if err != nil { + return false, "", err + } + var thisUcUserInfo *entity.UserExternalLogin + for _, t := range userInfoList { + if t.Provider == userCenter.Info().SlugName { + thisUcUserInfo = t + break + } + } + // If this user not login by user center, no need to check user status + if thisUcUserInfo == nil { + return true, "", nil + } + userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID) + if userStatus == plugin.UserStatusDeleted { + return false, thisUcUserInfo.ExternalID, nil + } + return true, thisUcUserInfo.ExternalID, nil +} diff --git a/internal/service/user_notification_config/user_notification_config_service.go b/internal/service/user_notification_config/user_notification_config_service.go new file mode 100644 index 000000000..7c54df0aa --- /dev/null +++ b/internal/service/user_notification_config/user_notification_config_service.go @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package user_notification_config + +import ( + "context" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + usercommon "github.com/apache/answer/internal/service/user_common" +) + +type UserNotificationConfigRepo interface { + Add(ctx context.Context, userIDs []string, source, channels string) (err error) + Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) + GetByUserID(ctx context.Context, userID string) ([]*entity.UserNotificationConfig, error) + GetBySource(ctx context.Context, source constant.NotificationSource) ([]*entity.UserNotificationConfig, error) + GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) ( + conf *entity.UserNotificationConfig, exist bool, err error) + GetByUsersAndSource(ctx context.Context, userIDs []string, source constant.NotificationSource) ( + []*entity.UserNotificationConfig, error) +} + +type UserNotificationConfigService struct { + userRepo usercommon.UserRepo + userNotificationConfigRepo UserNotificationConfigRepo +} + +func NewUserNotificationConfigService( + userRepo usercommon.UserRepo, + userNotificationConfigRepo UserNotificationConfigRepo, +) *UserNotificationConfigService { + return &UserNotificationConfigService{ + userRepo: userRepo, + userNotificationConfigRepo: userNotificationConfigRepo, + } +} + +func (us *UserNotificationConfigService) GetUserNotificationConfig(ctx context.Context, userID string) ( + resp *schema.GetUserNotificationConfigResp, err error) { + notificationConfigs, err := us.userNotificationConfigRepo.GetByUserID(ctx, userID) + if err != nil { + return nil, err + } + resp = &schema.GetUserNotificationConfigResp{} + resp.NotificationConfig = schema.NewNotificationConfig(notificationConfigs) + resp.Format() + return resp, nil +} + +func (us *UserNotificationConfigService) UpdateUserNotificationConfig( + ctx context.Context, req *schema.UpdateUserNotificationConfigReq) (err error) { + req.NotificationConfig.Format() + + err = us.userNotificationConfigRepo.Save(ctx, + us.convertToEntity(ctx, req.UserID, constant.InboxSource, req.NotificationConfig.Inbox)) + if err != nil { + return err + } + err = us.userNotificationConfigRepo.Save(ctx, + us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionSource, req.NotificationConfig.AllNewQuestion)) + if err != nil { + return err + } + err = us.userNotificationConfigRepo.Save(ctx, + us.convertToEntity(ctx, req.UserID, constant.AllNewQuestionForFollowingTagsSource, + req.NotificationConfig.AllNewQuestionForFollowingTags)) + if err != nil { + return err + } + return nil +} + +// SetDefaultUserNotificationConfig set default user notification config for user register +func (us *UserNotificationConfigService) SetDefaultUserNotificationConfig(ctx context.Context, userIDs []string) ( + err error) { + return us.userNotificationConfigRepo.Add(ctx, userIDs, + string(constant.InboxSource), `[{"key":"email","enable":true}]`) +} + +func (us *UserNotificationConfigService) convertToEntity(ctx context.Context, userID string, + source constant.NotificationSource, channel schema.NotificationChannelConfig) (c *entity.UserNotificationConfig) { + var channels schema.NotificationChannels + channels = append(channels, &channel) + c = &entity.UserNotificationConfig{ + UserID: userID, + Source: string(source), + Channels: channels.ToJsonString(), + } + for _, ch := range channels { + if ch.Enable { + c.Enabled = true + break + } + } + return c +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go deleted file mode 100644 index af2d4d6fc..000000000 --- a/internal/service/user_service.go +++ /dev/null @@ -1,550 +0,0 @@ -package service - -import ( - "context" - "encoding/hex" - "fmt" - "math/rand" - "regexp" - "strings" - - "github.com/Chain-Zhang/pinyin" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service/activity" - "github.com/answerdev/answer/internal/service/auth" - "github.com/answerdev/answer/internal/service/export" - "github.com/answerdev/answer/internal/service/service_config" - usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/answerdev/answer/pkg/checker" - "github.com/google/uuid" - "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" - "golang.org/x/crypto/bcrypt" -) - -// UserRepo user repository - -// UserService user service -type UserService struct { - userRepo usercommon.UserRepo - userActivity activity.UserActiveActivityRepo - serviceConfig *service_config.ServiceConfig - emailService *export.EmailService - authService *auth.AuthService -} - -func NewUserService(userRepo usercommon.UserRepo, - userActivity activity.UserActiveActivityRepo, - emailService *export.EmailService, - authService *auth.AuthService, - serviceConfig *service_config.ServiceConfig) *UserService { - return &UserService{ - userRepo: userRepo, - userActivity: userActivity, - emailService: emailService, - serviceConfig: serviceConfig, - authService: authService, - } -} - -// GetUserInfoByUserID get user info by user id -func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserResp, err error) { - userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) - if err != nil { - return nil, err - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - resp = &schema.GetUserResp{} - resp.GetFromUserEntity(userInfo) - resp.AccessToken = token - return resp, nil -} - -// GetUserStatus get user info by user id -func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) (resp *schema.GetUserStatusResp, err error) { - resp = &schema.GetUserStatusResp{} - if len(userID) == 0 { - return resp, nil - } - userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) - if err != nil { - return nil, err - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - - userCacheInfo := &entity.UserCacheInfo{ - UserID: userID, - UserStatus: userInfo.Status, - EmailStatus: userInfo.MailStatus, - } - err = us.authService.UpdateUserCacheInfo(ctx, token, userCacheInfo) - if err != nil { - return nil, err - } - resp = &schema.GetUserStatusResp{ - Status: schema.UserStatusShow[userInfo.Status], - } - return resp, nil -} - -func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( - resp *schema.GetOtherUserInfoResp, err error) { - userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) - if err != nil { - return nil, err - } - resp = &schema.GetOtherUserInfoResp{} - if !exist { - return resp, nil - } - resp.Has = true - resp.Info = &schema.GetOtherUserInfoByUsernameResp{} - resp.Info.GetFromUserEntity(userInfo) - return resp, nil -} - -// EmailLogin email login -func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogin) (resp *schema.GetUserResp, err error) { - userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) - if err != nil { - return nil, err - } - if !exist || userInfo.Status == entity.UserStatusDeleted { - return nil, errors.BadRequest(reason.EmailOrPasswordWrong) - } - if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) { - return nil, errors.BadRequest(reason.EmailOrPasswordWrong) - } - - err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID) - if err != nil { - log.Error("UpdateLastLoginDate", err.Error()) - } - - resp = &schema.GetUserResp{} - resp.GetFromUserEntity(userInfo) - userCacheInfo := &entity.UserCacheInfo{ - UserID: userInfo.ID, - EmailStatus: userInfo.MailStatus, - UserStatus: userInfo.Status, - } - resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) - if err != nil { - return nil, err - } - resp.IsAdmin = userInfo.IsAdmin - if resp.IsAdmin { - err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, userCacheInfo) - if err != nil { - return nil, err - } - } - - return resp, nil -} - -// RetrievePassWord . -func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRetrievePassWordRequest) (string, error) { - userInfo, has, err := us.userRepo.GetByEmail(ctx, req.Email) - if err != nil { - return "", err - } - if !has { - return "", errors.BadRequest(reason.UserNotFound) - } - - // send email - data := &schema.EmailCodeContent{ - Email: req.Email, - UserID: userInfo.ID, - } - code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailUrl) - if err != nil { - return "", err - } - go us.emailService.Send(ctx, req.Email, title, body, code, data.ToJSONString()) - return code, nil -} - -// UseRePassWord -func (us *UserService) UseRePassWord(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) { - data := &schema.EmailCodeContent{} - err = data.FromJSONString(req.Content) - if err != nil { - return nil, errors.BadRequest(reason.EmailVerifyUrlExpired) - } - - userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email) - if err != nil { - return nil, err - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - enpass, err := us.encryptPassword(ctx, req.Pass) - if err != nil { - return nil, err - } - userInfo.Pass = enpass - err = us.userRepo.UpdatePass(ctx, userInfo) - if err != nil { - return nil, err - } - resp = &schema.GetUserResp{} - return resp, nil -} - -func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) { - - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserId) - if err != nil { - return false, err - } - if !has { - return false, fmt.Errorf("user does not exist") - } - isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass) - if !isPass { - return false, nil - } - - return true, nil -} - -// UserModifyPassWord -func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.UserModifyPassWordRequest) error { - enpass, err := us.encryptPassword(ctx, request.Pass) - if err != nil { - return err - } - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserId) - if err != nil { - return err - } - if !has { - return fmt.Errorf("user does not exist") - } - isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass) - if !isPass { - return fmt.Errorf("the old password verification failed") - } - userInfo.Pass = enpass - err = us.userRepo.UpdatePass(ctx, userInfo) - if err != nil { - return err - } - return nil -} - -// UpdateInfo update user info -func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (err error) { - if len(req.Username) > 0 { - userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username) - if err != nil { - return err - } - if exist && userInfo.ID != req.UserId { - return errors.BadRequest(reason.UsernameDuplicate) - } - } - - userInfo := entity.User{} - userInfo.ID = req.UserId - userInfo.Avatar = req.Avatar - userInfo.DisplayName = req.DisplayName - userInfo.Bio = req.Bio - userInfo.BioHtml = req.BioHtml - userInfo.Location = req.Location - userInfo.Website = req.Website - userInfo.Username = req.Username - if err := us.userRepo.UpdateInfo(ctx, &userInfo); err != nil { - return err - } - return nil -} - -func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) { - _, has, err := us.userRepo.GetByEmail(ctx, email) - if err != nil { - return false, err - } - return has, nil -} - -// UserRegisterByEmail user register -func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( - resp *schema.GetUserResp, err error) { - _, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email) - if err != nil { - return nil, err - } - if has { - return nil, errors.BadRequest(reason.EmailDuplicate) - } - - userInfo := &entity.User{} - userInfo.EMail = registerUserInfo.Email - userInfo.DisplayName = registerUserInfo.Name - userInfo.Pass, err = us.encryptPassword(ctx, registerUserInfo.Pass) - if err != nil { - return nil, err - } - userInfo.Username, err = us.makeUsername(ctx, registerUserInfo.Name) - if err != nil { - return nil, err - } - userInfo.IPInfo = registerUserInfo.IP - userInfo.MailStatus = entity.EmailStatusToBeVerified - userInfo.Status = entity.UserStatusAvailable - err = us.userRepo.AddUser(ctx, userInfo) - if err != nil { - return nil, err - } - - // send email - data := &schema.EmailCodeContent{ - Email: registerUserInfo.Email, - UserID: userInfo.ID, - } - code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailUrl) - if err != nil { - return nil, err - } - go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString()) - - // return user info and token - resp = &schema.GetUserResp{} - resp.GetFromUserEntity(userInfo) - userCacheInfo := &entity.UserCacheInfo{ - UserID: userInfo.ID, - EmailStatus: userInfo.MailStatus, - UserStatus: userInfo.Status, - } - resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) - if err != nil { - return nil, err - } - resp.IsAdmin = userInfo.IsAdmin - if resp.IsAdmin { - err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) - if err != nil { - return nil, err - } - } - return resp, nil -} - -func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) error { - userInfo, has, err := us.userRepo.GetByUserID(ctx, userID) - if err != nil { - return err - } - if !has { - return errors.BadRequest(reason.UserNotFound) - } - - data := &schema.EmailCodeContent{ - Email: userInfo.EMail, - UserID: userInfo.ID, - } - code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailUrl) - if err != nil { - return err - } - go us.emailService.Send(ctx, userInfo.EMail, title, body, code, data.ToJSONString()) - return nil -} - -func (us *UserService) UserNoticeSet(ctx context.Context, userId string, noticeSwitch bool) ( - resp *schema.UserNoticeSetResp, err error) { - userInfo, has, err := us.userRepo.GetByUserID(ctx, userId) - if err != nil { - return nil, err - } - if !has { - return nil, errors.BadRequest(reason.UserNotFound) - } - if noticeSwitch { - userInfo.NoticeStatus = schema.Notice_Status_On - } else { - userInfo.NoticeStatus = schema.Notice_Status_Off - } - err = us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, userInfo.NoticeStatus) - return &schema.UserNoticeSetResp{NoticeSwitch: noticeSwitch}, err -} - -func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVerifyEmailReq) (resp *schema.GetUserResp, err error) { - data := &schema.EmailCodeContent{} - err = data.FromJSONString(req.Content) - if err != nil { - return nil, errors.BadRequest(reason.EmailVerifyUrlExpired) - } - - userInfo, has, err := us.userRepo.GetByEmail(ctx, data.Email) - if err != nil { - return nil, err - } - if !has { - return nil, errors.BadRequest(reason.UserNotFound) - } - userInfo.MailStatus = entity.EmailStatusAvailable - err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus) - if err != nil { - return nil, err - } - if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { - log.Error(err) - } - - resp = &schema.GetUserResp{} - resp.GetFromUserEntity(userInfo) - userCacheInfo := &entity.UserCacheInfo{ - UserID: userInfo.ID, - EmailStatus: userInfo.MailStatus, - UserStatus: userInfo.Status, - } - resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo) - if err != nil { - return nil, err - } - resp.IsAdmin = userInfo.IsAdmin - if resp.IsAdmin { - err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID}) - if err != nil { - return nil, err - } - } - return resp, nil -} - -// makeUsername -// Generate a unique Username based on the displayName -func (us *UserService) makeUsername(ctx context.Context, displayName string) (username string, err error) { - // Chinese processing - if has := checker.IsChinese(displayName); has { - str, err := pinyin.New(displayName).Split("").Mode(pinyin.WithoutTone).Convert() - if err != nil { - return "", err - } else { - displayName = str - } - } - - username = strings.ReplaceAll(displayName, " ", "_") - username = strings.ToLower(username) - suffix := "" - - re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`) - match := re.MatchString(username) - if !match { - return "", errors.BadRequest(reason.UsernameInvalid) - } - - for { - _, has, err := us.userRepo.GetByUsername(ctx, username+suffix) - if err != nil { - return "", err - } - if !has { - break - } - bytes := make([]byte, 2) - _, _ = rand.Read(bytes) - suffix = hex.EncodeToString(bytes) - } - return username + suffix, nil -} - -// verifyPassword -// Compare whether the password is correct -func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool { - err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass)) - if err != nil { - return false - } - return true -} - -// encryptPassword -// The password does irreversible encryption. -func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string, error) { - hashPwd, err := bcrypt.GenerateFromPassword([]byte(Pass), bcrypt.DefaultCost) - //This encrypted string can be saved to the database and can be used as password matching verification - return string(hashPwd), err -} - -// UserChangeEmailSendCode user change email verification -func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) error { - _, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) - if err != nil { - return err - } - if !exist { - return errors.BadRequest(reason.UserNotFound) - } - - _, exist, err = us.userRepo.GetByEmail(ctx, req.Email) - if err != nil { - return err - } - if exist { - return errors.BadRequest(reason.EmailDuplicate) - } - - data := &schema.EmailCodeContent{ - Email: req.Email, - UserID: req.UserID, - } - code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) - if err != nil { - return err - } - log.Infof("send email confirmation %s", verifyEmailUrl) - - go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString()) - return nil -} - -// UserChangeEmailVerify user change email verify code -func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (err error) { - data := &schema.EmailCodeContent{} - err = data.FromJSONString(content) - if err != nil { - return errors.BadRequest(reason.EmailVerifyUrlExpired) - } - - _, exist, err := us.userRepo.GetByEmail(ctx, data.Email) - if err != nil { - return err - } - if exist { - return errors.BadRequest(reason.EmailDuplicate) - } - - _, exist, err = us.userRepo.GetByUserID(ctx, data.UserID) - if err != nil { - return err - } - if !exist { - return errors.BadRequest(reason.UserNotFound) - } - err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email) - if err != nil { - return err - } - return nil -} diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go deleted file mode 100644 index 74789529c..000000000 --- a/internal/service/vote_service.go +++ /dev/null @@ -1,197 +0,0 @@ -package service - -import ( - "context" - - "github.com/answerdev/answer/internal/base/pager" - "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/activity_type" - "github.com/answerdev/answer/internal/service/comment_common" - "github.com/answerdev/answer/internal/service/config" - "github.com/answerdev/answer/internal/service/object_info" - "github.com/answerdev/answer/pkg/obj" - "github.com/segmentfault/pacman/log" - - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/internal/schema" - answercommon "github.com/answerdev/answer/internal/service/answer_common" - questioncommon "github.com/answerdev/answer/internal/service/question_common" - "github.com/answerdev/answer/internal/service/unique" - "github.com/segmentfault/pacman/errors" -) - -// VoteRepo activity repository -type VoteRepo interface { - VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) - GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) - ListUserVotes(ctx context.Context, userID string, req schema.GetVoteWithPageReq, activityTypes []int) (voteList []entity.Activity, total int64, err error) -} - -// VoteService user service -type VoteService struct { - voteRepo VoteRepo - UniqueIDRepo unique.UniqueIDRepo - configRepo config.ConfigRepo - questionRepo questioncommon.QuestionRepo - answerRepo answercommon.AnswerRepo - commentCommonRepo comment_common.CommentCommonRepo - objectService *object_info.ObjService -} - -func NewVoteService( - VoteRepo VoteRepo, - uniqueIDRepo unique.UniqueIDRepo, - configRepo config.ConfigRepo, - questionRepo questioncommon.QuestionRepo, - answerRepo answercommon.AnswerRepo, - commentCommonRepo comment_common.CommentCommonRepo, - objectService *object_info.ObjService, -) *VoteService { - return &VoteService{ - voteRepo: VoteRepo, - UniqueIDRepo: uniqueIDRepo, - configRepo: configRepo, - questionRepo: questionRepo, - answerRepo: answerRepo, - commentCommonRepo: commentCommonRepo, - objectService: objectService, - } -} - -// VoteUp vote up -func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = as.GetObjectUserId(ctx, dto.ObjectID) - if err != nil { - return - } - - // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return - } - - if dto.IsCancel { - return as.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) - } else { - return as.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID) - } -} - -// VoteDown vote down -func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { - voteResp = &schema.VoteResp{} - - var objectUserID string - - objectUserID, err = as.GetObjectUserId(ctx, dto.ObjectID) - if err != nil { - return - } - - // check user is voting self or not - if objectUserID == dto.UserID { - err = errors.BadRequest(reason.DisallowVoteYourSelf) - return - } - - if dto.IsCancel { - return as.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) - } else { - return as.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID) - } -} - -func (vs *VoteService) GetObjectUserId(ctx context.Context, objectID string) (userID string, err error) { - var objectKey string - objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) - - if err != nil { - err = nil - return - } - - switch objectKey { - case "question": - object, has, e := vs.questionRepo.GetQuestion(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "answer": - object, has, e := vs.answerRepo.GetAnswer(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - case "comment": - object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID) - if e != nil || !has { - err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack() - return - } - userID = object.UserID - default: - err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack() - return - } - - return -} - -// ListUserVotes list user's votes -func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (model *pager.PageModel, err error) { - var ( - resp []schema.GetVoteWithPageResp - typeKeys = []string{ - "question.vote_up", - "question.vote_down", - "answer.vote_up", - "answer.vote_down", - } - activityTypes []int - ) - - for _, typeKey := range typeKeys { - t, err := vs.configRepo.GetConfigType(typeKey) - if err != nil { - continue - } - activityTypes = append(activityTypes, t) - } - - voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req, activityTypes) - if err != nil { - return - } - - for _, voteInfo := range voteList { - objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) - if err != nil { - log.Error(err) - } - - item := schema.GetVoteWithPageResp{ - CreatedAt: voteInfo.CreatedAt.Unix(), - ObjectID: objInfo.ObjectID, - QuestionID: objInfo.QuestionID, - AnswerID: objInfo.AnswerID, - ObjectType: objInfo.ObjectType, - Title: objInfo.Title, - Content: objInfo.Content, - VoteType: activity_type.Format(voteInfo.ActivityType), - } - resp = append(resp, item) - } - - return pager.NewPageModel(total, resp), err -} diff --git a/licenserc.toml b/licenserc.toml new file mode 100644 index 000000000..4e1b4b7e5 --- /dev/null +++ b/licenserc.toml @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +headerPath = "Apache-2.0-ASF.txt" + +excludes = [ + "docs/release/**", + "ui/build/**", + "answer-data/**", + "NOTICE", + "DISCLAIMER", + "Makefile", + "go.mod", + "go.sum", + "ui/.eslintignore", + "ui/.browserslistrc", + "ui/.npmrc", + "ui/.env.*", + "script/plugin_list", + "charts/templates/_helpers.tpl", + "charts/.helmignore", +] diff --git a/pkg/checker/chinese.go b/pkg/checker/chinese.go index 3a6ebd228..9af4ab8ee 100644 --- a/pkg/checker/chinese.go +++ b/pkg/checker/chinese.go @@ -1,14 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package checker import "unicode" func IsChinese(str string) bool { - var count int for _, v := range str { if unicode.Is(unicode.Han, v) { - count++ - break + return true } } - return count > 0 + return false } diff --git a/pkg/checker/email.go b/pkg/checker/email.go new file mode 100644 index 000000000..a9732fdf0 --- /dev/null +++ b/pkg/checker/email.go @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import "strings" + +func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool { + if len(allowEmailDomains) == 0 { + return true + } + + for _, domain := range allowEmailDomains { + if strings.HasSuffix(email, domain) { + return true + } + } + + return false +} diff --git a/pkg/checker/file_type.go b/pkg/checker/file_type.go new file mode 100644 index 000000000..51f687d6c --- /dev/null +++ b/pkg/checker/file_type.go @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import ( + "fmt" + "image" + _ "image/gif" // use init to support decode jpeg,jpg,png,gif + _ "image/jpeg" + _ "image/png" + "io" + "os" + "path/filepath" + "strings" + + "github.com/segmentfault/pacman/log" + "golang.org/x/image/webp" +) + +// IsUnAuthorizedExtension check whether the file extension is not in the allowedExtensions +// WANING Only checks the file extension is not reliable, but `http.DetectContentType` and `mimetype` are not reliable for all file types. +func IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool { + ext := strings.ToLower(strings.Trim(filepath.Ext(fileName), ".")) + for _, extension := range allowedExtensions { + if extension == ext { + return false + } + } + return true +} + +// DecodeAndCheckImageFile currently answers support image type is +// `image/jpeg, image/jpg, image/png, image/gif, image/webp` +func DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool { + ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), ".")) + switch ext { + case "jpg", "jpeg", "png", "gif": // only allow for `image/jpeg, image/jpg, image/png, image/gif` + if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageConfigCheck) { + return false + } + if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageCheck) { + return false + } + case "webp": + if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageConfigCheck) { + return false + } + if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageCheck) { + return false + } + } + return true +} + +func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checker func(file io.Reader, maxImageMegapixel int) error) bool { + file, err := os.Open(localFilePath) + if err != nil { + log.Errorf("open file error: %v", err) + return false + } + defer file.Close() + + if err = checker(file, maxImageMegapixel); err != nil { + log.Errorf("check image format error: %v", err) + return false + } + return true +} + +func standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error { + config, _, err := image.DecodeConfig(file) + if err != nil { + return fmt.Errorf("decode image config error: %v", err) + } + if imageSizeTooLarge(config, maxImageMegapixel) { + return fmt.Errorf("image size too large") + } + return nil +} + +func standardImageCheck(file io.Reader, maxImageMegapixel int) error { + _, _, err := image.Decode(file) + if err != nil { + return fmt.Errorf("decode image error: %v", err) + } + return nil +} + +func webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error { + config, err := webp.DecodeConfig(file) + if err != nil { + return fmt.Errorf("decode webp image config error: %v", err) + } + if imageSizeTooLarge(config, maxImageMegapixel) { + return fmt.Errorf("image size too large") + } + return nil +} + +func webpImageCheck(file io.Reader, maxImageMegapixel int) error { + _, err := webp.Decode(file) + if err != nil { + return fmt.Errorf("decode webp image error: %v", err) + } + return nil +} + +func imageSizeTooLarge(config image.Config, maxImageMegapixel int) bool { + return config.Width*config.Height > maxImageMegapixel +} diff --git a/pkg/checker/password.go b/pkg/checker/password.go index 6c1bd6e65..ac274f352 100644 --- a/pkg/checker/password.go +++ b/pkg/checker/password.go @@ -1,8 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package checker import ( "fmt" "regexp" + "strings" ) const ( @@ -13,27 +33,26 @@ const ( LevelS ) -// CheckPassword -// minLength: Specifies the minimum length of a password -// maxLength:Specifies the maximum length of a password -// minLevel:Specifies the minimum strength level required for passwords -// pwd:Text passwords -func CheckPassword(minLength, maxLength, minLevel int, pwd string) error { - // First check whether the password length is within the range - if len(pwd) < minLength { - return fmt.Errorf("BAD PASSWORD: The password is shorter than %d characters", minLength) - } - if len(pwd) > maxLength { - return fmt.Errorf("BAD PASSWORD: The password is logner than %d characters", maxLength) +const ( + PasswordCannotContainSpaces = "error.password.space_invalid" +) + +// CheckPassword checks the password strength +func CheckPassword(password string) error { + if strings.Contains(password, " ") { + return fmt.Errorf(PasswordCannotContainSpaces) } + // TODO Currently there is no requirement for password strength + minLevel := 0 + // The password strength level is initialized to D. // The regular is used to verify the password strength. // If the matching is successful, the password strength increases by 1 - var level int = levelD + level := levelD patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`} for _, pattern := range patternList { - match, _ := regexp.MatchString(pattern, pwd) + match, _ := regexp.MatchString(pattern, password) if match { level++ } @@ -41,7 +60,7 @@ func CheckPassword(minLength, maxLength, minLevel int, pwd string) error { // If the final password strength falls below the required minimum strength, return with an error if level < minLevel { - return fmt.Errorf("The password does not satisfy the current policy requirements. ") + return fmt.Errorf("the password does not satisfy the current policy requirements") } return nil } diff --git a/pkg/checker/path_ignore.go b/pkg/checker/path_ignore.go new file mode 100644 index 000000000..8be757be5 --- /dev/null +++ b/pkg/checker/path_ignore.go @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import ( + "sync" + + "github.com/apache/answer/configs" + "github.com/segmentfault/pacman/log" + "gopkg.in/yaml.v3" +) + +type PathIgnore struct { + Users []string `yaml:"users"` + Questions []string `yaml:"questions"` +} + +var ( + ignorePathInit sync.Once + pathIgnore = &PathIgnore{} +) + +func initPathIgnore() { + if err := yaml.Unmarshal(configs.PathIgnore, pathIgnore); err != nil { + log.Error(err) + } +} + +// IsUsersIgnorePath checks whether the username is in ignore path +func IsUsersIgnorePath(username string) bool { + ignorePathInit.Do(initPathIgnore) + for _, u := range pathIgnore.Users { + if u == username { + return true + } + } + return false +} + +// IsQuestionsIgnorePath checks whether the questionID is in ignore path +func IsQuestionsIgnorePath(questionID string) bool { + ignorePathInit.Do(initPathIgnore) + for _, u := range pathIgnore.Questions { + if u == questionID { + return true + } + } + return false +} diff --git a/pkg/checker/question_link.go b/pkg/checker/question_link.go new file mode 100644 index 000000000..41b246c36 --- /dev/null +++ b/pkg/checker/question_link.go @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/pkg/obj" + "github.com/apache/answer/pkg/uid" +) + +const ( + QuestionLinkTypeURL = 1 + QuestionLinkTypeID = 2 +) + +type QuestionLink struct { + LinkType int + QuestionID string + AnswerID string +} + +func GetQuestionLink(content string) []QuestionLink { + uniqueIDs := make(map[string]struct{}) + var questionLinks []QuestionLink + + // use two pointer to find the link + left, right := 0, 0 + for right < len(content) { + // find "/questions/" or "#" + if right+11 < len(content) && content[right:right+11] == "/questions/" { + left = right + right += 11 + processURL(content, &left, &right, uniqueIDs, &questionLinks) + } else if content[right] == '#' { + left = right + 1 + right = left + processID(content, &left, &right, uniqueIDs, &questionLinks) + } else { + right++ + } + } + + return questionLinks +} + +func processURL(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { + for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { + *right++ + } + questionID := content[*left+len("/questions/") : *right] + + answerID := "" + if *right < len(content) && content[*right] == '/' { + *left = *right + 1 + *right = *left + for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { + *right++ + } + answerID = content[*left:*right] + } + + addUniqueID(questionID, answerID, QuestionLinkTypeURL, uniqueIDs, questionLinks) +} + +func processID(content string, left, right *int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { + for *right < len(content) && (isDigit(content[*right]) || isLetter(content[*right])) { + *right++ + } + id := content[*left:*right] + addUniqueID(id, "", QuestionLinkTypeID, uniqueIDs, questionLinks) +} + +func isDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func isLetter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +func addUniqueID(questionID, answerID string, linkType int, uniqueIDs map[string]struct{}, questionLinks *[]QuestionLink) { + isAdd := false + if answerID != "" { + objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(answerID)) + if err != nil { + answerID = "" + } + + if objectType == constant.AnswerObjectType { + if _, ok := uniqueIDs[answerID]; !ok { + uniqueIDs[answerID] = struct{}{} + isAdd = true + } + } + } + + if objectType, err := obj.GetObjectTypeStrByObjectID(uid.DeShortID(questionID)); err == nil { + if _, ok := uniqueIDs[questionID]; !ok { + uniqueIDs[questionID] = struct{}{} + isAdd = true + if objectType == constant.AnswerObjectType { + answerID = questionID + questionID = "" + } + } + } + + if isAdd { + *questionLinks = append(*questionLinks, QuestionLink{ + LinkType: linkType, + QuestionID: questionID, + AnswerID: answerID, + }) + } +} diff --git a/pkg/checker/question_link_test.go b/pkg/checker/question_link_test.go new file mode 100644 index 000000000..d3ff0625c --- /dev/null +++ b/pkg/checker/question_link_test.go @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker_test + +import ( + "testing" + + "github.com/apache/answer/pkg/checker" + "github.com/stretchr/testify/assert" +) + +func TestGetQuestionLink(t *testing.T) { + // Step 1: Test empty content + t.Run("Empty content", func(t *testing.T) { + links := checker.GetQuestionLink("") + assert.Empty(t, links) + }) + + // Step 2: Test content without link or ID + t.Run("Content without link or ID", func(t *testing.T) { + links := checker.GetQuestionLink("This is a random text") + assert.Empty(t, links) + }) + + // Step 3: Test content with valid question link + t.Run("Valid question link", func(t *testing.T) { + links := checker.GetQuestionLink("Check this question: https://example.com/questions/10010000000000060") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "", + }, + }, links) + }) + + // Step 4: Test content with valid question and answer link + t.Run("Valid question and answer link", func(t *testing.T) { + links := checker.GetQuestionLink("Check this answer: https://example.com/questions/10010000000000060/10020000000000060?from=copy") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "10020000000000060", + }, + }, links) + }) + + // Step 5: Test content with #questionID + t.Run("Content with #questionID", func(t *testing.T) { + links := checker.GetQuestionLink("This is question #10010000000000060") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeID, + QuestionID: "10010000000000060", + AnswerID: "", + }, + }, links) + }) + + // Step 6: Test content with #answerID + t.Run("Content with #answerID", func(t *testing.T) { + links := checker.GetQuestionLink("This is answer #10020000000000060") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeID, + QuestionID: "", + AnswerID: "10020000000000060", + }, + }, links) + }) + + // Step 7: Test invalid question ID + t.Run("Invalid question ID", func(t *testing.T) { + links := checker.GetQuestionLink("https://example.com/questions/invalid") + assert.Empty(t, links) + }) + + // Step 8: Test invalid answer ID + t.Run("Invalid answer ID", func(t *testing.T) { + links := checker.GetQuestionLink("https://example.com/questions/10010000000000060/invalid") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "", + }, + }, links) + }) + + // Step 9: Test content with multiple links and IDs + t.Run("Multiple links and IDs", func(t *testing.T) { + content := "Question #10010000000000060 and https://example.com/questions/10010000000000060/10020000000000061 and https://example.com/questions/10010000000000065/10020000000000066 and another #10020000000000066" + links := checker.GetQuestionLink(content) + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeID, + QuestionID: "10010000000000060", + AnswerID: "", + }, + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "10020000000000061", + }, + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000065", + AnswerID: "10020000000000066", + }, + }, links) + }) + + // Step 11: Test URL with www prefix + t.Run("URL with www prefix", func(t *testing.T) { + links := checker.GetQuestionLink("Check this question: https://www.example.com/questions/10010000000000060") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "", + }, + }, links) + }) + + // Step 12: Test URL without protocol + t.Run("URL without protocol", func(t *testing.T) { + links := checker.GetQuestionLink("Check this question: example.com/questions/10010000000000060") + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000060", + AnswerID: "", + }, + }, links) + }) + + // Step 14: Test error id + t.Run("Error id", func(t *testing.T) { + links := checker.GetQuestionLink("https://example.com/questions/10110000000000060") + assert.Empty(t, links) + }) + + // step 15: SEO options + t.Run("SEO options", func(t *testing.T) { + content := ` + URL1: http://localhost:3000/questions/D1I2 + URL2: http://localhost:3000/questions/D1I2/hello + URL3: http://localhost:3000/questions/10010000000000068 + URL4: http://localhost:3000/questions/10010000000000068/hello + ERROR URL: http://localhost:3000/questions/AAAA/BBBB + ` + links := checker.GetQuestionLink(content) + assert.Equal(t, []checker.QuestionLink{ + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "D1I2", + AnswerID: "", + }, + { + LinkType: checker.QuestionLinkTypeURL, + QuestionID: "10010000000000068", + AnswerID: "", + }, + }, links) + }) +} diff --git a/pkg/checker/reserved_username.go b/pkg/checker/reserved_username.go new file mode 100644 index 000000000..6dfd34710 --- /dev/null +++ b/pkg/checker/reserved_username.go @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" + + "github.com/apache/answer/configs" + "github.com/apache/answer/internal/cli" + "github.com/apache/answer/pkg/dir" +) + +var ( + reservedUsernameMapping = make(map[string]bool) + reservedUsernameInit sync.Once +) + +func initReservedUsername() { + reservedUsernamesJsonFilePath := filepath.Join(cli.ConfigFileDir, cli.DefaultReservedUsernamesConfigFileName) + if dir.CheckFileExist(reservedUsernamesJsonFilePath) { + // if reserved username file exists, read it and replace configuration + reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath) + if err == nil { + configs.ReservedUsernames = reservedUsernamesJsonFile + } + } + var usernames []string + _ = json.Unmarshal(configs.ReservedUsernames, &usernames) + for _, username := range usernames { + reservedUsernameMapping[username] = true + } +} + +// IsReservedUsername checks whether the username is reserved +func IsReservedUsername(username string) bool { + reservedUsernameInit.Do(initReservedUsername) + return reservedUsernameMapping[username] +} diff --git a/pkg/checker/url.go b/pkg/checker/url.go new file mode 100644 index 000000000..b096d8ada --- /dev/null +++ b/pkg/checker/url.go @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import ( + "net/url" + "strings" +) + +func IsURL(str string) bool { + s := strings.ToLower(str) + + if len(s) == 0 { + return false + } + + u, err := url.Parse(s) + if err != nil || u.Scheme == "" { + return false + } + + if u.Host == "" && u.Fragment == "" && u.Opaque == "" { + return false + } + return u.Scheme == "http" || u.Scheme == "https" +} diff --git a/pkg/checker/username.go b/pkg/checker/username.go new file mode 100644 index 000000000..cf554118f --- /dev/null +++ b/pkg/checker/username.go @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +import "regexp" + +var ( + usernameReg = regexp.MustCompile(`^[\w.\- ]{2,30}$`) +) + +func IsInvalidUsername(username string) bool { + return !usernameReg.MatchString(username) +} diff --git a/pkg/checker/zero_string.go b/pkg/checker/zero_string.go new file mode 100644 index 000000000..7789cf632 --- /dev/null +++ b/pkg/checker/zero_string.go @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package checker + +// IsNotZeroString check s is not empty string and is not "0" +func IsNotZeroString(s string) bool { + return len(s) > 0 && s != "0" +} + +// FilterEmptyString filter empty string from string slice +func FilterEmptyString(strs []string) []string { + var result []string + for _, str := range strs { + if IsNotZeroString(str) { + result = append(result, str) + } + } + return result +} diff --git a/pkg/converter/array.go b/pkg/converter/array.go new file mode 100644 index 000000000..2b122203e --- /dev/null +++ b/pkg/converter/array.go @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package converter + +func ArrayNotInArray(original []string, search []string) []string { + var result []string + originalMap := make(map[string]bool) + for _, v := range original { + originalMap[v] = true + } + for _, v := range search { + if _, ok := originalMap[v]; !ok { + result = append(result, v) + } + } + return result +} + +func UniqueArray[T comparable](input []T) []T { + result := make([]T, 0, len(input)) + seen := make(map[T]bool, len(input)) + for _, element := range input { + if !seen[element] { + result = append(result, element) + seen[element] = true + } + } + return result +} diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go new file mode 100644 index 000000000..1789a32cd --- /dev/null +++ b/pkg/converter/markdown.go @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package converter + +import ( + "bytes" + "regexp" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/microcosm-cc/bluemonday" + "github.com/segmentfault/pacman/log" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + goldmarkHTML "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +// Markdown2HTML convert markdown to html +func Markdown2HTML(source string) string { + mdConverter := goldmark.New( + goldmark.WithExtensions(&DangerousHTMLFilterExtension{}, extension.GFM, extension.Footnote), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + goldmarkHTML.WithHardWraps(), + ), + ) + var buf bytes.Buffer + if err := mdConverter.Convert([]byte(source), &buf); err != nil { + log.Error(err) + return source + } + html := buf.String() + filter := bluemonday.UGCPolicy() + filter.AllowStyling() + filter.RequireNoFollowOnLinks(false) + filter.RequireParseableURLs(false) + filter.RequireNoFollowOnFullyQualifiedLinks(false) + filter.AllowElements("kbd") + filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally() + filter.AllowAttrs("start").OnElements("ol") + html = strings.TrimSpace(filter.Sanitize(html)) + return html +} + +// Markdown2BasicHTML convert markdown to html, Only basic syntax can be used +func Markdown2BasicHTML(source string) string { + content := Markdown2HTML(source) + filter := bluemonday.NewPolicy() + filter.AllowElements("p", "b", "br", "strong", "em") + filter.AllowAttrs("src").OnElements("img") + filter.AddSpaceWhenStrippingTag(true) + content = filter.Sanitize(content) + return content +} + +type DangerousHTMLFilterExtension struct { +} + +func (e *DangerousHTMLFilterExtension) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(&DangerousHTMLRenderer{ + Config: goldmarkHTML.NewConfig(), + Filter: bluemonday.UGCPolicy(), + }, 1), + )) +} + +type DangerousHTMLRenderer struct { + goldmarkHTML.Config + Filter *bluemonday.Policy +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) + reg.Register(ast.KindRawHTML, r.renderRawHTML) + reg.Register(ast.KindLink, r.renderLink) + reg.Register(ast.KindAutoLink, r.renderAutoLink) +} + +func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkSkipChildren, nil + } + n := node.(*ast.RawHTML) + l := n.Segments.Len() + for i := 0; i < l; i++ { + segment := n.Segments.At(i) + if string(source[segment.Start:segment.Stop]) == "" || string(source[segment.Start:segment.Stop]) == "" { + _, _ = w.Write(segment.Value(source)) + } else { + _, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source))) + } + } + return ast.WalkSkipChildren, nil +} + +func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.HTMLBlock) + if entering { + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + r.Writer.SecureWrite(w, line.Value(source)) + } + } else { + if n.HasClosure() { + closure := n.ClosureLine + r.Writer.SecureWrite(w, closure.Value(source)) + } + } + return ast.WalkContinue, nil +} + +func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Link) + if entering && r.renderLinkIsUrl(string(n.Destination)) { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("") + } + return ast.WalkContinue, nil +} + +func (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.AutoLink) + + if !entering || !r.renderLinkIsUrl(string(n.URL(source))) { + return ast.WalkContinue, nil + } + _, _ = w.WriteString(`') + } else { + _, _ = w.WriteString(`">`) + } + _, _ = w.Write(util.EscapeHTML(label)) + _, _ = w.WriteString(``) + return ast.WalkContinue, nil +} + +func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool { + isURL := govalidator.IsURL(verifyUrl) + isPath, _ := regexp.MatchString(`^/`, verifyUrl) + return isURL || isPath +} diff --git a/pkg/converter/str.go b/pkg/converter/str.go index 74d9b2ec2..40c147fdc 100644 --- a/pkg/converter/str.go +++ b/pkg/converter/str.go @@ -1,7 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package converter import ( "fmt" + "github.com/segmentfault/pacman/log" "strconv" ) @@ -24,3 +44,30 @@ func StringToInt(str string) int { func IntToString(data int64) string { return fmt.Sprintf("%d", data) } + +// InterfaceToString converts data to string +// It will be used in template render +func InterfaceToString(data interface{}) string { + switch t := data.(type) { + case int: + i := data.(int) + return strconv.Itoa(i) + case int8: + i := data.(int8) + return strconv.Itoa(int(i)) + case int16: + i := data.(int16) + return strconv.Itoa(int(i)) + case int32: + i := data.(int32) + return string(i) + case int64: + i := data.(int64) + return strconv.FormatInt(i, 10) + case string: + return data.(string) + default: + log.Warn("can't convert type:", t) + } + return "" +} diff --git a/pkg/converter/user.go b/pkg/converter/user.go new file mode 100644 index 000000000..b5c5042dc --- /dev/null +++ b/pkg/converter/user.go @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package converter + +import "github.com/segmentfault/pacman/utils" + +func DeleteUserDisplay(userID string) string { + return utils.EnShortID(StringToInt64(userID), 100) +} diff --git a/pkg/day/day.go b/pkg/day/day.go new file mode 100644 index 000000000..90afac0ad --- /dev/null +++ b/pkg/day/day.go @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package day + +import ( + "time" +) + +var placeholder = map[string]string{ + "YY": "06", // 06 year + "YYYY": "2006", // 2006 full year + "M": "1", // 1-12 month + "MM": "01", // 01-12 month + "MMM": "Jan", // Jan-Dec month + "MMMM": "January", // January-December month + "D": "2", // 1-31 date + "DD": "02", // 01-31 date preset 0 + "H": "15", // 00-23 hour 24 + "HH": "15", // 00-23 hour 24 + "h": "3", // 1-12 hour 12 + "hh": "03", // 01-12 hour 12 + "m": "4", // 0-59 minute + "mm": "04", // 00-59 minute + "s": "5", // 0-59 second + "ss": "05", // 00-59 second + "A": "PM", // AM / PM + "a": "pm", // am / pm + "[at]": "at", // at string +} + +func Format(unix int64, format, tz string) (formatted string) { + /*l := len(placeholders) - 1 + for i := l; i >= 0; i-- { + format = strings.ReplaceAll(format, placeholders[i].old, placeholders[i].new) + }*/ + toFormat := "" + from := []rune(format) + for len(from) > 0 { + to, suffix := nextStdChunk(from) + toFormat += string(to) + from = suffix + } + + _, _ = time.LoadLocation(tz) + formatted = time.Unix(unix, 0).Format(toFormat) + return +} + +func nextStdChunk(from []rune) (to, suffix []rune) { + if len(from) == 0 { + to = []rune{} + suffix = []rune{} + return + } + + s := string(from[0]) + old := "" + + switch s { + case "Y": + if len(from) >= 4 && string(from[:4]) == "YYYY" { + old = "YYYY" + } else if len(from) >= 2 && string(from[:2]) == "YY" { + old = "YY" + } + case "M": + for i := 4; i > 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "MMMM": + old = "MMMM" + case "MMM": + old = "MMM" + case "MM": + old = "MM" + case "M": + old = "M" + } + } + if old != "" { + break + } + } + case "D": + for i := 2; i >= 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "DD": + old = "DD" + case "D": + old = "D" + } + } + if old != "" { + break + } + } + case "H": + for i := 2; i >= 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "HH": + old = "HH" + case "H": + old = "H" + } + } + if old != "" { + break + } + } + case "h": + for i := 2; i >= 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "hh": + old = "hh" + case "h": + old = "h" + } + } + if old != "" { + break + } + } + case "m": + for i := 2; i >= 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "mm": + old = "mm" + case "m": + old = "m" + } + } + if old != "" { + break + } + } + case "s": + for i := 2; i >= 0; i-- { + if len(from) >= i { + switch string(from[:i]) { + case "ss": + old = "ss" + case "s": + old = "s" + } + } + if old != "" { + break + } + } + case "A": + old = "A" + case "a": + old = "a" + case "[": + if len(from) >= 4 && string(from[:4]) == "[at]" { + old = "[at]" + } + default: + old = s + } + + tos, ok := placeholder[old] + if !ok { + to = []rune(old) + } else { + to = []rune(tos) + } + + suffix = from[len([]rune(old)):] + return +} diff --git a/pkg/day/day_test.go b/pkg/day/day_test.go new file mode 100644 index 000000000..73e49aca3 --- /dev/null +++ b/pkg/day/day_test.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package day + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestFormat(t *testing.T) { + sec := time.Now().Unix() + tz := "Asia/Shanghai" + actual := Format(sec, "YYYY-MM-DD HH:mm:ss", tz) + _, _ = time.LoadLocation(tz) + expected := time.Unix(sec, 0).Format("2006-01-02 15:04:05") + assert.Equal(t, expected, actual) +} diff --git a/pkg/dir/dir.go b/pkg/dir/dir.go index 559f0d1c1..928883c2e 100644 --- a/pkg/dir/dir.go +++ b/pkg/dir/dir.go @@ -1,6 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package dir -import "os" +import ( + "fmt" + "os" + "path/filepath" +) func CreateDirIfNotExist(path string) error { return os.MkdirAll(path, os.ModePerm) @@ -15,3 +38,32 @@ func CheckFileExist(path string) bool { f, err := os.Stat(path) return err == nil && !f.IsDir() } + +func DirSize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if !info.IsDir() { + size += info.Size() + } + return err + }) + return size, err +} + +func FormatFileSize(fileSize int64) (size string) { + if fileSize < 1024 { + //return strconv.FormatInt(fileSize, 10) + "B" + return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1)) + } else if fileSize < (1024 * 1024) { + return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024)) + } else if fileSize < (1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024)) + } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { + return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024)) + } else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) + return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) + } + +} diff --git a/pkg/display/url.go b/pkg/display/url.go new file mode 100644 index 000000000..11574f0a5 --- /dev/null +++ b/pkg/display/url.go @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package display + +import ( + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/pkg/htmltext" + "github.com/apache/answer/pkg/uid" +) + +// QuestionURL get question url +func QuestionURL(permalink int, siteUrl, questionID, title string) string { + u := siteUrl + "/questions" + if permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionID { + questionID = uid.DeShortID(questionID) + } else { + questionID = uid.EnShortID(questionID) + } + u += "/" + questionID + if permalink == constant.PermalinkQuestionIDAndTitle || permalink == constant.PermalinkQuestionIDAndTitleByShortID { + u += "/" + htmltext.UrlTitle(title) + } + return u +} + +// AnswerURL get answer url +func AnswerURL(permalink int, siteUrl, questionID, title, answerID string) string { + if permalink == constant.PermalinkQuestionIDAndTitle || + permalink == constant.PermalinkQuestionID { + answerID = uid.DeShortID(answerID) + } else { + answerID = uid.EnShortID(answerID) + } + return QuestionURL(permalink, siteUrl, questionID, title) + "/" + answerID +} + +// CommentURL get comment url +func CommentURL(permalink int, siteUrl, questionID, title, answerID, commentID string) string { + if len(answerID) > 0 { + return AnswerURL(permalink, siteUrl, questionID, answerID, title) + "?commentId=" + commentID + } + return QuestionURL(permalink, siteUrl, questionID, title) + "?commentId=" + commentID +} + +// UserURL get user url +func UserURL(siteUrl, username string) string { + return siteUrl + "/users/" + username +} diff --git a/pkg/encryption/md5.go b/pkg/encryption/md5.go new file mode 100644 index 000000000..b6f1723d4 --- /dev/null +++ b/pkg/encryption/md5.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package encryption + +import ( + "crypto/md5" + "encoding/hex" +) + +// MD5 return md5 hash +func MD5(data string) string { + h := md5.New() + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/pkg/gravatar/gravatar.go b/pkg/gravatar/gravatar.go new file mode 100644 index 000000000..9c79b3872 --- /dev/null +++ b/pkg/gravatar/gravatar.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package gravatar + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net/url" + "strings" +) + +// GetAvatarURL get avatar url from gravatar by email +func GetAvatarURL(baseURL, email string) string { + hasher := sha256.Sum256([]byte(strings.TrimSpace(email))) + hash := hex.EncodeToString(hasher[:]) + return baseURL + hash +} + +// Resize resize avatar by pixel +func Resize(originalAvatarURL string, sizePixel int) (resizedAvatarURL string) { + if len(originalAvatarURL) == 0 { + return + } + originalURL, err := url.Parse(originalAvatarURL) + if err != nil { + return originalAvatarURL + } + query := originalURL.Query() + query.Set("s", fmt.Sprintf("%d", sizePixel)) + originalURL.RawQuery = query.Encode() + return originalURL.String() +} diff --git a/pkg/gravatar/gravatar_test.go b/pkg/gravatar/gravatar_test.go new file mode 100644 index 000000000..bf504e68f --- /dev/null +++ b/pkg/gravatar/gravatar_test.go @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package gravatar + +import ( + "github.com/apache/answer/internal/base/constant" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetAvatarURL(t *testing.T) { + type args struct { + email string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "answer@answer.com", + args: args{email: "answer@answer.com"}, + want: "https://www.gravatar.com/avatar/7296942c1f63d97f6c124705142009867638f7b3dbcdadd0cb1bcb40e427eb8e", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, GetAvatarURL(constant.DefaultGravatarBaseURL, tt.args.email)) + }) + } +} + +func TestResize(t *testing.T) { + type args struct { + originalAvatarURL string + sizePixel int + } + tests := []struct { + name string + args args + wantResizedAvatarURL string + }{ + { + name: "original url", + args: args{ + originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7", + sizePixel: 128, + }, + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", + }, + { + name: "already resized url", + args: args{ + originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", + sizePixel: 64, + }, + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=64", + }, + { + name: "empty url", + args: args{ + originalAvatarURL: "", + sizePixel: 64, + }, + wantResizedAvatarURL: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.wantResizedAvatarURL, Resize(tt.args.originalAvatarURL, tt.args.sizePixel), "Resize(%v, %v)", tt.args.originalAvatarURL, tt.args.sizePixel) + }) + } +} diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go new file mode 100644 index 000000000..49049109d --- /dev/null +++ b/pkg/htmltext/htmltext.go @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package htmltext + +import ( + "io" + "net/http" + "net/url" + "regexp" + "strings" + "unicode/utf8" + + "github.com/Machiel/slugify" + "github.com/apache/answer/pkg/checker" + "github.com/apache/answer/pkg/converter" + strip "github.com/grokify/html-strip-tags-go" + "github.com/mozillazg/go-pinyin" +) + +// ClearText clear HTML, get the clear text +func ClearText(html string) (text string) { + if len(html) == 0 { + text = html + return + } + + var ( + re *regexp.Regexp + codeReg = `(?ism)<(pre)>.*<\/pre>` + codeRepl = "{code...}" + linkReg = `(?ism)(.*)?<\/a>` + linkRepl = " [$1] " + spaceReg = ` +` + spaceRepl = " " + ) + re = regexp.MustCompile(codeReg) + html = re.ReplaceAllString(html, codeRepl) + + re = regexp.MustCompile(linkReg) + html = re.ReplaceAllString(html, linkRepl) + + text = strings.NewReplacer( + "\n", " ", + "\r", " ", + "\t", " ", + ).Replace(strip.StripTags(html)) + + // replace multiple spaces to one space + re = regexp.MustCompile(spaceReg) + text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl)) + return +} + +func UrlTitle(title string) (text string) { + title = convertChinese(title) + title = clearEmoji(title) + title = slugify.Slugify(title) + title = url.QueryEscape(title) + title = cutLongTitle(title) + if len(title) == 0 { + title = "topic" + } + return title +} + +func clearEmoji(s string) string { + ret := "" + rs := []rune(s) + for i := 0; i < len(rs); i++ { + if len(string(rs[i])) != 4 { + ret += string(rs[i]) + } + } + return ret +} + +func convertChinese(content string) string { + has := checker.IsChinese(content) + if !has { + return content + } + return strings.Join(pinyin.LazyConvert(content, nil), "-") +} + +func cutLongTitle(title string) string { + if len(title) > 150 { + return title[0:150] + } + return title +} + +// FetchExcerpt return the excerpt from the HTML string +func FetchExcerpt(html, trimMarker string, limit int) (text string) { + return FetchRangedExcerpt(html, trimMarker, 0, limit) +} + +// findFirstMatchedWord returns the first matched word and its index +func findFirstMatchedWord(text string, words []string) (string, int) { + if len(text) == 0 || len(words) == 0 { + return "", 0 + } + + words = converter.UniqueArray(words) + firstWord := "" + firstIndex := len(text) + + for _, word := range words { + if idx := strings.Index(text, word); idx != -1 && idx < firstIndex { + firstIndex = idx + firstWord = word + } + } + + if firstIndex != len(text) { + return firstWord, firstIndex + } + + return "", 0 +} + +// getRuneRange returns the valid begin and end indexes of the runeText +func getRuneRange(runeText []rune, offset, limit int) (begin, end int) { + runeLen := len(runeText) + + limit = min(runeLen, max(0, limit)) + begin = min(runeLen, max(0, offset)) + end = min(runeLen, begin+limit) + + return +} + +// FetchRangedExcerpt returns a ranged excerpt from the HTML string. +// Note: offset is a rune index, not a byte index +func FetchRangedExcerpt(html, trimMarker string, offset int, limit int) (text string) { + if len(html) == 0 { + text = html + return + } + + runeText := []rune(ClearText(html)) + begin, end := getRuneRange(runeText, offset, limit) + text = string(runeText[begin:end]) + + if begin > 0 { + text = trimMarker + text + } + if end < len(runeText) { + text = text + trimMarker + } + + return +} + +// FetchMatchedExcerpt returns the matched excerpt according to the words +func FetchMatchedExcerpt(html string, words []string, trimMarker string, trimLength int) string { + text := ClearText(html) + matchedWord, matchedIndex := findFirstMatchedWord(text, words) + runeIndex := utf8.RuneCountInString(text[0:matchedIndex]) + + trimLength = max(0, trimLength) + runeOffset := runeIndex - trimLength + runeLimit := trimLength + trimLength + utf8.RuneCountInString(matchedWord) + + textRuneCount := utf8.RuneCountInString(text) + if runeOffset+runeLimit > textRuneCount { + // Reserved extra chars before the matched word + runeOffset = textRuneCount - runeLimit + } + + return FetchRangedExcerpt(html, trimMarker, runeOffset, runeLimit) +} + +func GetPicByUrl(Url string) string { + res, err := http.Get(Url) + if err != nil { + return "" + } + defer res.Body.Close() + pix, err := io.ReadAll(res.Body) + if err != nil { + return "" + } + return string(pix) +} diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go new file mode 100644 index 000000000..d549d8874 --- /dev/null +++ b/pkg/htmltext/htmltext_test.go @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package htmltext + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClearText(t *testing.T) { + var ( + expected, + clearedText string + ) + + // test code clear text + expected = "hello{code...}" + clearedText = ClearText("

hello

var a = \"good\"

") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello [example.com]" + clearedText = ClearText("

hello example.com

") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("
hello
\n
world
") + assert.Equal(t, expected, clearedText) +} + +func TestFetchExcerpt(t *testing.T) { + var ( + expected, + text string + ) + + // test english string + expected = "hello..." + text = FetchExcerpt("

hello world

", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好..." + text = FetchExcerpt("

hello你好world

", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂..." + text = FetchExcerpt("

hello你好😂world

", "...", 8) + assert.Equal(t, expected, text) + + expected = "hello你好" + text = FetchExcerpt("

hello你好

", "...", 8) + assert.Equal(t, expected, text) +} + +func TestUrlTitle(t *testing.T) { + list := []string{ + "hello你好😂...", + "这是一个,标题,title", + } + for _, title := range list { + formatTitle := UrlTitle(title) + fmt.Println(formatTitle) + } +} + +func TestFindFirstMatchedWord(t *testing.T) { + var ( + expectedWord, + actualWord string + expectedIndex, + actualIndex int + ) + + text := "Hello, I have 中文 and 😂 and I am supposed to work fine." + + // test find nothing + expectedWord, expectedIndex = "", 0 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"youcantfindme"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) + + // test find one word + expectedWord, expectedIndex = "文", 17 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"文"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) + + // test find multiple matched words + expectedWord, expectedIndex = "Hello", 0 + actualWord, actualIndex = findFirstMatchedWord(text, []string{"Hello", "文"}) + assert.Equal(t, expectedWord, actualWord) + assert.Equal(t, expectedIndex, actualIndex) +} + +func TestGetRuneRange(t *testing.T) { + var ( + expectedBegin, + expectedEnd, + actualBegin, + actualEnd int + ) + + runeText := []rune("Hello, I have 中文 and 😂.") + runeLen := len(runeText) + + // test get range of negative offset and negative limit + expectedBegin, expectedEnd = 0, 0 + actualBegin, actualEnd = getRuneRange(runeText, -1, -1) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of exceeding offset and exceeding limit + expectedBegin, expectedEnd = runeLen, runeLen + actualBegin, actualEnd = getRuneRange(runeText, runeLen+1, runeLen+1) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of normal offset and exceeding limit + expectedBegin, expectedEnd = 3, runeLen + actualBegin, actualEnd = getRuneRange(runeText, 3, runeLen) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) + + // test get range of normal offset and normal limit + expectedBegin, expectedEnd = 3, 10 + actualBegin, actualEnd = getRuneRange(runeText, 3, 7) + assert.Equal(t, expectedBegin, actualBegin) + assert.Equal(t, expectedEnd, actualEnd) +} + +func TestFetchRangedExcerpt(t *testing.T) { + var ( + expected, + actual string + ) + + // test english string + expected = "hello..." + actual = FetchRangedExcerpt("

hello world

", "...", 0, 5) + assert.Equal(t, expected, actual) + + // test string with offset + expected = "...llo你好..." + actual = FetchRangedExcerpt("

hello你好world

", "...", 2, 5) + assert.Equal(t, expected, actual) + + // test mixed string with emoticon with offset + expected = "...你好😂..." + actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 3) + assert.Equal(t, expected, actual) + + // test mixed string with offset and exceeding limit + expected = "...你好😂world" + actual = FetchRangedExcerpt("

hello你好😂world

", "...", 5, 100) + assert.Equal(t, expected, actual) +} + +func TestFetchMatchedExcerpt(t *testing.T) { + var ( + expected, + actual string + ) + + html := "

Hello, I have 中文 and 😂 and I am supposed to work fine

" + + // test find nothing + // it should return from the begin with double trimLength text + expected = "Hello, I h..." + actual = FetchMatchedExcerpt(html, []string{"youcantfindme"}, "...", 5) + assert.Equal(t, expected, actual) + + // test find the word at the end + // it should return the word beginning with double trimLenth plus len(word) + expected = "... work fine" + actual = FetchMatchedExcerpt(html, []string{"youcant", "fine"}, "...", 3) + assert.Equal(t, expected, actual) + + // test find multiple words + // it should return the first matched word with trimmedText + expected = "... have 中文 and 😂..." + actual = FetchMatchedExcerpt(html, []string{"中文", "😂"}, "...", 6) + assert.Equal(t, expected, actual) +} diff --git a/pkg/obj/obj.go b/pkg/obj/obj.go index 931fa9fdf..0dc628191 100644 --- a/pkg/obj/obj.go +++ b/pkg/obj/obj.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package obj import ( - "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" - "github.com/answerdev/answer/pkg/converter" + "github.com/apache/answer/internal/base/constant" + "github.com/apache/answer/internal/base/reason" + "github.com/apache/answer/pkg/converter" "github.com/segmentfault/pacman/errors" ) diff --git a/pkg/random/random_username.go b/pkg/random/random_username.go new file mode 100644 index 000000000..7f41caf15 --- /dev/null +++ b/pkg/random/random_username.go @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package random + +import ( + "crypto/rand" + "encoding/hex" +) + +func UsernameSuffix() string { + bytes := make([]byte, 2) + _, _ = rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +func Username() string { + bytes := make([]byte, 6) + _, _ = rand.Read(bytes) + return hex.EncodeToString(bytes) +} diff --git a/pkg/token/token.go b/pkg/token/token.go index 7e675304b..f783a10d8 100644 --- a/pkg/token/token.go +++ b/pkg/token/token.go @@ -1,9 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package token import "github.com/google/uuid" // GenerateToken generate token func GenerateToken() string { - uid, _ := uuid.NewUUID() + uid, _ := uuid.NewV7() return uid.String() } diff --git a/pkg/uid/id.go b/pkg/uid/id.go index bd29f0833..f2c72baed 100644 --- a/pkg/uid/id.go +++ b/pkg/uid/id.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package uid import ( @@ -15,9 +34,9 @@ type SnowFlakeID struct { var snowFlakeIDGenerator *SnowFlakeID func init() { - //todo - rand.Seed(time.Now().UnixNano()) - node, err := snowflake.NewNode(int64(rand.Intn(1000)) + 1) + source := rand.NewSource(time.Now().UnixNano()) + r := rand.New(source) + node, err := snowflake.NewNode(int64(r.Intn(1000)) + 1) if err != nil { panic(err.Error()) } diff --git a/pkg/uid/sid.go b/pkg/uid/sid.go new file mode 100644 index 000000000..4f09c5df7 --- /dev/null +++ b/pkg/uid/sid.go @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package uid + +import ( + "strconv" + + "github.com/segmentfault/pacman/utils" +) + +const salt = int64(100) + +// NumToShortID num to string +func NumToShortID(id int64) string { + sid := strconv.FormatInt(id, 10) + if len(sid) < 17 { + return "" + } + sTypeCode := sid[1:4] + sid = sid[4:int32(len(sid))] + id, err := strconv.ParseInt(sid, 10, 64) + if err != nil { + return "" + } + typeCode, err := strconv.ParseInt(sTypeCode, 10, 64) + if err != nil { + return "" + } + code := utils.EnShortID(id, salt) + tcode := utils.EnShortID(typeCode, salt) + return tcode + code +} + +// ShortIDToNum string to num +func ShortIDToNum(code string) int64 { + if len(code) < 2 { + return 0 + } + scodeType := code[0:2] + code = code[2:int32(len(code))] + + id := utils.DeShortID(code, salt) + codeType := utils.DeShortID(scodeType, salt) + return 10000000000000000 + codeType*10000000000000 + id +} + +func EnShortID(id string) string { + num, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return id + } + return NumToShortID(num) +} + +func DeShortID(sid string) string { + num, err := strconv.ParseInt(sid, 10, 64) + if err != nil { + return strconv.FormatInt(ShortIDToNum(sid), 10) + } + if num < 10000000000000000 { + return strconv.FormatInt(ShortIDToNum(sid), 10) + } + return sid +} + +func IsShortID(id string) bool { + num, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return true + } + if num < 10000000000000000 { + return true + } + return false +} diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 000000000..ff08d2592 --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package writer + +import ( + "bufio" + "os" +) + +// ReplaceFile remove old file and write new file +func ReplaceFile(filePath, content string) error { + _ = os.Remove(filePath) + return WriteFile(filePath, content) +} + +// WriteFile write file to path +func WriteFile(filePath, content string) error { + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + return err + } + defer func() { + _ = file.Close() + }() + writer := bufio.NewWriter(file) + if _, err := writer.WriteString(content); err != nil { + return err + } + if err := writer.Flush(); err != nil { + return err + } + return nil +} + +// MoveFile move file to new path +func MoveFile(oldPath, newPath string) error { + return os.Rename(oldPath, newPath) +} diff --git a/plugin/agent.go b/plugin/agent.go new file mode 100644 index 000000000..aee9f8be4 --- /dev/null +++ b/plugin/agent.go @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "github.com/gin-gonic/gin" +) + +type Agent interface { + Base + RegisterUnAuthRouter(r *gin.RouterGroup) + RegisterAuthUserRouter(r *gin.RouterGroup) + RegisterAuthAdminRouter(r *gin.RouterGroup) +} + +var ( + CallAgent, + registerAgent = MakePlugin[Agent](true) + siteURLFn func() string +) + +// SiteURL The site url is the domain address of the current site. e.g. http://localhost:8080 +// When some Agent plugins want to redirect to the origin site, it can use this function to get the site url. +func SiteURL() string { + if siteURLFn != nil { + return siteURLFn() + } + return "" +} + +// RegisterGetSiteURLFunc Register a function to get the site url. +func RegisterGetSiteURLFunc(fn func() string) { + siteURLFn = fn +} diff --git a/plugin/base.go b/plugin/base.go new file mode 100644 index 000000000..76e09b104 --- /dev/null +++ b/plugin/base.go @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +// Info presents the plugin information +type Info struct { + Name Translator + SlugName string + Description Translator + Author string + Version string + Link string +} + +// Base is the base plugin +type Base interface { + // Info returns the plugin information + Info() Info +} + +var ( + // CallBase is a function that calls all registered base plugins + CallBase, + registerBase = MakePlugin[Base](true) +) diff --git a/plugin/cache.go b/plugin/cache.go new file mode 100644 index 000000000..676b6ac3e --- /dev/null +++ b/plugin/cache.go @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "context" + "time" +) + +type Cache interface { + Base + + GetString(ctx context.Context, key string) (data string, exist bool, err error) + SetString(ctx context.Context, key, value string, ttl time.Duration) (err error) + GetInt64(ctx context.Context, key string) (data int64, exist bool, err error) + SetInt64(ctx context.Context, key string, value int64, ttl time.Duration) (err error) + Increase(ctx context.Context, key string, value int64) (data int64, err error) + Decrease(ctx context.Context, key string, value int64) (data int64, err error) + Del(ctx context.Context, key string) (err error) + Flush(ctx context.Context) (err error) +} + +var ( + // CallCache is a function that calls all registered cache + CallCache, + registerCache = MakePlugin[Cache](false) +) diff --git a/plugin/captcha.go b/plugin/captcha.go new file mode 100644 index 000000000..96047e67f --- /dev/null +++ b/plugin/captcha.go @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type Captcha interface { + Base + // GetConfig required. Get the captcha plugin configuration. + // The configuration is used to generate the captcha for frontend. Such as the token for third-party service. + GetConfig() (configJsonStr string) + // Create optional. If this plugin need to create captcha via backend, implement this method. + // On other hand, if this plugin create captcha via third-party service, ignore this method. + // Return captcha: The captcha image base64 string, code: The real captcha code. + Create() (captcha, code string) + // Verify required. Verify the user input captcha is correct or not + // captcha: The captchaCode generated by Create method, if not implemented, it's empty. + Verify(captchaCode, userInput string) (pass bool) +} + +var ( + // CallCaptcha is a function that calls all registered parsers + callCaptcha, + registerCaptcha = MakePlugin[Captcha](false) +) + +func CallCaptcha(fn func(fn Captcha) error) error { + slugName := "" + _ = callCaptcha(func(captcha Captcha) error { + slugName = captcha.Info().SlugName + return nil + }) + if slugName == "" { + return nil + } + return callCaptcha(func(captcha Captcha) error { + if captcha.Info().SlugName == slugName { + return fn(captcha) + } + return nil + }) +} + +func CaptchaEnabled() (enabled bool) { + _ = callCaptcha(func(fn Captcha) error { + enabled = true + return nil + }) + return +} + +func coordinatedCaptchaPlugins(slugName string) (enabledSlugNames []string) { + isCaptcha := false + _ = callCaptcha(func(captcha Captcha) error { + name := captcha.Info().SlugName + if slugName == name { + isCaptcha = true + } else { + enabledSlugNames = append(enabledSlugNames, name) + } + return nil + }) + if isCaptcha { + return enabledSlugNames + } + return nil +} diff --git a/plugin/cdn.go b/plugin/cdn.go new file mode 100644 index 000000000..ac1b4c135 --- /dev/null +++ b/plugin/cdn.go @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +var ( + DefaultCDNFileType = map[string]bool{ + ".ico": true, + ".json": true, + ".css": true, + ".js": true, + ".webp": true, + ".woff2": true, + ".woff": true, + ".jpg": true, + ".svg": true, + ".png": true, + ".map": true, + ".txt": true, + } +) + +type CDN interface { + Base + GetStaticPrefix() string +} + +var ( + // CallCDN is a function that calls all registered parsers + CallCDN, + registerCDN = MakePlugin[CDN](false) +) + +func coordinatedCDNPlugins(slugName string) (enabledSlugNames []string) { + isCDN := false + _ = CallCDN(func(cdn CDN) error { + name := cdn.Info().SlugName + if slugName == name { + isCDN = true + } else { + enabledSlugNames = append(enabledSlugNames, name) + } + return nil + }) + if isCDN { + return enabledSlugNames + } + return nil +} diff --git a/plugin/config.go b/plugin/config.go new file mode 100644 index 000000000..b03e794ee --- /dev/null +++ b/plugin/config.go @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type ConfigType string +type InputType string + +const ( + ConfigTypeInput ConfigType = "input" + ConfigTypeTextarea ConfigType = "textarea" + ConfigTypeCheckbox ConfigType = "checkbox" + ConfigTypeRadio ConfigType = "radio" + ConfigTypeSelect ConfigType = "select" + ConfigTypeUpload ConfigType = "upload" + ConfigTypeTimezone ConfigType = "timezone" + ConfigTypeSwitch ConfigType = "switch" + ConfigTypeButton ConfigType = "button" + ConfigTypeLegend ConfigType = "legend" +) + +const ( + InputTypeText InputType = "text" + InputTypeColor InputType = "color" + InputTypeDate InputType = "date" + InputTypeDatetime InputType = "datetime-local" + InputTypeEmail InputType = "email" + InputTypeMonth InputType = "month" + InputTypeNumber InputType = "number" + InputTypePassword InputType = "password" + InputTypeRange InputType = "range" + InputTypeSearch InputType = "search" + InputTypeTel InputType = "tel" + InputTypeTime InputType = "time" + InputTypeUrl InputType = "url" + InputTypeWeek InputType = "week" +) + +type ConfigField struct { + Name string `json:"name"` + Type ConfigType `json:"type"` + Title Translator `json:"title"` + Description Translator `json:"description"` + Required bool `json:"required"` + Value any `json:"value"` + UIOptions ConfigFieldUIOptions `json:"ui_options"` + Options []ConfigFieldOption `json:"options,omitempty"` +} + +type ConfigFieldUIOptions struct { + Placeholder Translator `json:"placeholder,omitempty"` + Rows string `json:"rows,omitempty"` + InputType InputType `json:"input_type,omitempty"` + Label Translator `json:"label,omitempty"` + Action *UIOptionAction `json:"action,omitempty"` + Variant string `json:"variant,omitempty"` + Text Translator `json:"text,omitempty"` + ClassName string `json:"class_name,omitempty"` + FieldClassName string `json:"field_class_name,omitempty"` +} + +type ConfigFieldOption struct { + Label Translator `json:"label"` + Value string `json:"value"` +} + +type UIOptionAction struct { + Url string `json:"url"` + Method string `json:"method,omitempty"` + Loading *LoadingAction `json:"loading,omitempty"` + OnComplete *OnCompleteAction `json:"on_complete,omitempty"` +} + +const ( + LoadingActionStateNone LoadingActionType = "none" + LoadingActionStatePending LoadingActionType = "pending" + LoadingActionStateComplete LoadingActionType = "completed" +) + +type LoadingActionType string + +type LoadingAction struct { + Text Translator `json:"text"` + State LoadingActionType `json:"state"` +} + +type OnCompleteAction struct { + ToastReturnMessage bool `json:"toast_return_message"` + RefreshFormConfig bool `json:"refresh_form_config"` +} + +type Config interface { + Base + + // ConfigFields returns the list of config fields + ConfigFields() []ConfigField + + // ConfigReceiver receives the config data, it calls when the config is saved or initialized. + // We recommend to unmarshal the data to a struct, and then use the struct to do something. + // The config is encoded in JSON format. + // It depends on the definition of ConfigFields. + ConfigReceiver(config []byte) error +} + +var ( + // CallConfig is a function that calls all registered config plugins + CallConfig, + registerConfig = MakePlugin[Config](true) +) diff --git a/plugin/connector.go b/plugin/connector.go new file mode 100644 index 000000000..267cd3916 --- /dev/null +++ b/plugin/connector.go @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type Connector interface { + Base + + // ConnectorLogoSVG presents the logo in svg format + ConnectorLogoSVG() string + + // ConnectorName presents the name of the connector + // e.g. Facebook, Twitter, Instagram + ConnectorName() Translator + + // ConnectorSlugName presents the slug name of the connector + // Please use lowercase and hyphen as the separator + // e.g. facebook, twitter, instagram + ConnectorSlugName() string + + // ConnectorSender presents the sender of the connector + // It handles the start endpoint of the connector + // receiverURL is the whole URL of the receiver + ConnectorSender(ctx *GinContext, receiverURL string) (redirectURL string) + + // ConnectorReceiver presents the receiver of the connector + // It handles the callback endpoint of the connector, and returns the + ConnectorReceiver(ctx *GinContext, receiverURL string) (userInfo ExternalLoginUserInfo, err error) +} + +// ExternalLoginUserInfo external login user info +type ExternalLoginUserInfo struct { + // required. The unique user ID provided by the third-party login + ExternalID string + // optional. This name is used preferentially during registration + DisplayName string + // optional. This username is used preferentially during registration + Username string + // optional. If email exist will bind the existing user + // IMPORTANT: The email must have been verified. If the plugin can't guarantee the email is verified, please leave it empty. + Email string + // optional. The avatar URL provided by the third-party login platform + Avatar string + // optional. The original user information provided by the third-party login platform + MetaInfo string +} + +var ( + // CallConnector is a function that calls all registered connectors + CallConnector, + registerConnector = MakePlugin[Connector](false) +) diff --git a/plugin/embed.go b/plugin/embed.go new file mode 100644 index 000000000..772599ce8 --- /dev/null +++ b/plugin/embed.go @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import "github.com/gin-gonic/gin" + +type EmbedConfig struct { + Platform string `json:"platform"` + Enable bool `json:"enable"` +} + +type Embed interface { + Base + GetEmbedConfigs(ctx *gin.Context) (embedConfigs []*EmbedConfig, err error) +} + +var ( + // CallEmbed is a function that calls all registered parsers + CallEmbed, + registerEmbed = MakePlugin[Embed](false) +) diff --git a/plugin/filter.go b/plugin/filter.go new file mode 100644 index 000000000..c1573a748 --- /dev/null +++ b/plugin/filter.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type Filter interface { + Base + FilterText(text string) (err error) +} + +var ( + // CallFilter is a function that calls all registered parsers + CallFilter, + registerFilter = MakePlugin[Filter](false) +) diff --git a/plugin/importer.go b/plugin/importer.go new file mode 100644 index 000000000..bfd7d36a5 --- /dev/null +++ b/plugin/importer.go @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "context" +) + +type QuestionImporterInfo struct { + Title string `json:"title"` + Content string `json:"content"` + Tags []string `json:"tags"` + UserEmail string `json:"user_email"` +} + +type Importer interface { + Base + RegisterImporterFunc(ctx context.Context, importer ImporterFunc) +} + +type ImporterFunc interface { + AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error) +} + +var ( + // CallImporter is a function that calls all registered parsers + CallImporter, + registerImporter = MakePlugin[Importer](false) +) + +func ImporterEnabled() (enabled bool) { + _ = CallImporter(func(fn Importer) error { + enabled = true + return nil + }) + return +} +func GetImporter() (ip Importer, ok bool) { + _ = CallImporter(func(fn Importer) error { + ip = fn + ok = true + return nil + }) + return +} diff --git a/plugin/kv_storage.go b/plugin/kv_storage.go new file mode 100644 index 000000000..d1ed3eaa6 --- /dev/null +++ b/plugin/kv_storage.go @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "context" + "fmt" + "math/rand/v2" + "time" + + "github.com/apache/answer/internal/entity" + "github.com/segmentfault/pacman/cache" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" + "xorm.io/xorm" +) + +// Error variables for KV storage operations +var ( + // ErrKVKeyNotFound is returned when the requested key does not exist in the KV storage + ErrKVKeyNotFound = fmt.Errorf("key not found in KV storage") + // ErrKVGroupEmpty is returned when a required group name is empty + ErrKVGroupEmpty = fmt.Errorf("group name is empty") + // ErrKVKeyEmpty is returned when a required key name is empty + ErrKVKeyEmpty = fmt.Errorf("key name is empty") + // ErrKVKeyAndGroupEmpty is returned when both key and group names are empty + ErrKVKeyAndGroupEmpty = fmt.Errorf("both key and group are empty") + // ErrKVTransactionFailed is returned when a KV storage transaction operation fails + ErrKVTransactionFailed = fmt.Errorf("KV storage transaction failed") +) + +// KVParams is the parameters for KV storage operations +type KVParams struct { + Group string + Key string + Value string + Page int + PageSize int +} + +// KVOperator provides methods to interact with the key-value storage system for plugins +type KVOperator struct { + data *Data + session *xorm.Session + pluginSlugName string + cacheTTL time.Duration +} + +// KVStorageOption defines a function type that configures a KVOperator +type KVStorageOption func(*KVOperator) + +// WithCacheTTL is the option to set the cache TTL; the default value is 30 minutes. +// If ttl is less than 0, the cache will not be used +func WithCacheTTL(ttl time.Duration) KVStorageOption { + return func(kv *KVOperator) { + kv.cacheTTL = ttl + } +} + +// Option is used to set the options for the KV storage +func (kv *KVOperator) Option(opts ...KVStorageOption) { + for _, opt := range opts { + opt(kv) + } +} + +func (kv *KVOperator) getSession(ctx context.Context) (*xorm.Session, func()) { + session := kv.session + cleanup := func() {} + if session == nil { + session = kv.data.DB.NewSession().Context(ctx) + cleanup = func() { + if session != nil { + session.Close() + } + } + } + return session, cleanup +} + +func (kv *KVOperator) getCacheKey(params KVParams) string { + return fmt.Sprintf("plugin_kv_storage:%s:group:%s:key:%s", kv.pluginSlugName, params.Group, params.Key) +} + +func (kv *KVOperator) setCache(ctx context.Context, params KVParams) { + if kv.cacheTTL < 0 { + return + } + + ttl := kv.cacheTTL + if ttl > 10 { + ttl += time.Duration(float64(ttl) * 0.1 * (1 - rand.Float64())) + } + + cacheKey := kv.getCacheKey(params) + if err := kv.data.Cache.SetString(ctx, cacheKey, params.Value, ttl); err != nil { + log.Warnf("cache set failed: %v, key: %s", err, cacheKey) + } +} + +func (kv *KVOperator) getCache(ctx context.Context, params KVParams) (string, bool, error) { + if kv.cacheTTL < 0 { + return "", false, nil + } + + cacheKey := kv.getCacheKey(params) + return kv.data.Cache.GetString(ctx, cacheKey) +} + +func (kv *KVOperator) cleanCache(ctx context.Context, params KVParams) { + if kv.cacheTTL < 0 { + return + } + + if err := kv.data.Cache.Del(ctx, kv.getCacheKey(params)); err != nil { + log.Warnf("Failed to delete cache for key %s: %v", params.Key, err) + } +} + +// Get retrieves a value from KV storage by group and key. +// Returns the value as a string or an error if the key is not found. +func (kv *KVOperator) Get(ctx context.Context, params KVParams) (string, error) { + if params.Key == "" { + return "", ErrKVKeyEmpty + } + + if value, exist, err := kv.getCache(ctx, params); err == nil && exist { + return value, nil + } + + // query + data := entity.PluginKVStorage{} + query, cleanup := kv.getSession(ctx) + defer cleanup() + + query.Where(builder.Eq{ + "plugin_slug_name": kv.pluginSlugName, + "`group`": params.Group, + "`key`": params.Key, + }) + + has, err := query.Get(&data) + if err != nil { + return "", err + } + if !has { + return "", ErrKVKeyNotFound + } + + params.Value = data.Value + kv.setCache(ctx, params) + + return data.Value, nil +} + +// Set stores a value in KV storage with the specified group and key. +// Updates the value if it already exists. +func (kv *KVOperator) Set(ctx context.Context, params KVParams) error { + if params.Key == "" { + return ErrKVKeyEmpty + } + + query, cleanup := kv.getSession(ctx) + defer cleanup() + + data := &entity.PluginKVStorage{ + PluginSlugName: kv.pluginSlugName, + Group: params.Group, + Key: params.Key, + Value: params.Value, + } + + kv.cleanCache(ctx, params) + + affected, err := query.Where(builder.Eq{ + "plugin_slug_name": kv.pluginSlugName, + "`group`": params.Group, + "`key`": params.Key, + }).Cols("value").Update(data) + if err != nil { + return err + } + + if affected == 0 { + _, err = query.Insert(data) + if err != nil { + return err + } + } + return nil +} + +// Del removes values from KV storage by group and/or key. +// If both group and key are provided, only that specific entry is deleted. +// If only group is provided, all entries in that group are deleted. +// At least one of group or key must be provided. +func (kv *KVOperator) Del(ctx context.Context, params KVParams) error { + if params.Key == "" && params.Group == "" { + return ErrKVKeyAndGroupEmpty + } + + kv.cleanCache(ctx, params) + + session, cleanup := kv.getSession(ctx) + defer cleanup() + + session.Where(builder.Eq{ + "plugin_slug_name": kv.pluginSlugName, + }) + if params.Group != "" { + session.Where(builder.Eq{"`group`": params.Group}) + } + if params.Key != "" { + session.Where(builder.Eq{"`key`": params.Key}) + } + + _, err := session.Delete(&entity.PluginKVStorage{}) + return err +} + +// GetByGroup retrieves all key-value pairs for a specific group with pagination support. +// Returns a map of keys to values or an error if the group is empty or not found. +func (kv *KVOperator) GetByGroup(ctx context.Context, params KVParams) (map[string]string, error) { + if params.Group == "" { + return nil, ErrKVGroupEmpty + } + + if params.Page < 1 { + params.Page = 1 + } + if params.PageSize < 1 { + params.PageSize = 10 + } + + query, cleanup := kv.getSession(ctx) + defer cleanup() + + var items []entity.PluginKVStorage + err := query.Where(builder.Eq{"plugin_slug_name": kv.pluginSlugName, "`group`": params.Group}). + Limit(params.PageSize, (params.Page-1)*params.PageSize). + OrderBy("id ASC"). + Find(&items) + if err != nil { + return nil, err + } + + result := make(map[string]string, len(items)) + for _, item := range items { + result[item.Key] = item.Value + } + + return result, nil +} + +// Tx executes a function within a transaction context. If the KVOperator already has a session, +// it will use that session. Otherwise, it creates a new transaction session. +// The transaction will be committed if the function returns nil, or rolled back if it returns an error. +func (kv *KVOperator) Tx(ctx context.Context, fn func(ctx context.Context, kv *KVOperator) error) error { + var ( + txKv = kv + shouldCommit bool + ) + + if kv.session == nil { + session := kv.data.DB.NewSession().Context(ctx) + if err := session.Begin(); err != nil { + session.Close() + return fmt.Errorf("%w: begin transaction failed: %v", ErrKVTransactionFailed, err) + } + + defer func() { + if !shouldCommit { + if rollbackErr := session.Rollback(); rollbackErr != nil { + log.Errorf("rollback failed: %v", rollbackErr) + } + } + session.Close() + }() + + txKv = &KVOperator{ + session: session, + data: kv.data, + pluginSlugName: kv.pluginSlugName, + } + shouldCommit = true + } + + if err := fn(ctx, txKv); err != nil { + return fmt.Errorf("%w: %v", ErrKVTransactionFailed, err) + } + + if shouldCommit { + if err := txKv.session.Commit(); err != nil { + return fmt.Errorf("%w: commit failed: %v", ErrKVTransactionFailed, err) + } + } + return nil +} + +// KVStorage defines the interface for plugins that need data storage capabilities +type KVStorage interface { + Info() Info + SetOperator(operator *KVOperator) +} + +var ( + CallKVStorage, + registerKVStorage = MakePlugin[KVStorage](true) +) + +// NewKVOperator creates a new KV storage operator with the specified database engine, cache and plugin name. +// It returns a KVOperator instance that can be used to interact with the plugin's storage. +func NewKVOperator(db *xorm.Engine, cache cache.Cache, pluginSlugName string) *KVOperator { + return &KVOperator{ + data: &Data{DB: db, Cache: cache}, + pluginSlugName: pluginSlugName, + cacheTTL: 30 * time.Minute, + } +} diff --git a/plugin/notification.go b/plugin/notification.go new file mode 100644 index 000000000..4c483bcd4 --- /dev/null +++ b/plugin/notification.go @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +// NotificationType is the type of the notification +type NotificationType string + +const ( + NotificationUpdateQuestion NotificationType = "notification.action.update_question" + NotificationAnswerTheQuestion NotificationType = "notification.action.answer_the_question" + NotificationUpVotedTheQuestion NotificationType = "notification.action.up_voted_question" + NotificationDownVotedTheQuestion NotificationType = "notification.action.down_voted_question" + NotificationUpdateAnswer NotificationType = "notification.action.update_answer" + NotificationAcceptAnswer NotificationType = "notification.action.accept_answer" + NotificationUpVotedTheAnswer NotificationType = "notification.action.up_voted_answer" + NotificationDownVotedTheAnswer NotificationType = "notification.action.down_voted_answer" + NotificationCommentQuestion NotificationType = "notification.action.comment_question" + NotificationCommentAnswer NotificationType = "notification.action.comment_answer" + NotificationUpVotedTheComment NotificationType = "notification.action.up_voted_comment" + NotificationReplyToYou NotificationType = "notification.action.reply_to_you" + NotificationMentionYou NotificationType = "notification.action.mention_you" + NotificationYourQuestionIsClosed NotificationType = "notification.action.your_question_is_closed" + NotificationYourQuestionWasDeleted NotificationType = "notification.action.your_question_was_deleted" + NotificationYourAnswerWasDeleted NotificationType = "notification.action.your_answer_was_deleted" + NotificationYourCommentWasDeleted NotificationType = "notification.action.your_comment_was_deleted" + NotificationInvitedYouToAnswer NotificationType = "notification.action.invited_you_to_answer" + NotificationNewQuestion NotificationType = "notification.action.new_question" + NotificationNewQuestionFollowedTag NotificationType = "notification.action.new_question_followed_tag" +) + +type Notification interface { + Base + + // GetNewQuestionSubscribers returns the subscribers of the new question notification + GetNewQuestionSubscribers() (userIDs []string) + + // Notify sends a notification to the user + Notify(msg NotificationMessage) +} + +type NotificationMessage struct { + // the type of the notification + Type NotificationType `json:"notification_type"` + // the receiver user id + ReceiverUserID string `json:"receiver_user_id"` + // the receiver user using language + ReceiverLang string `json:"receiver_lang"` + // the receiver user external id (optional) + ReceiverExternalID string `json:"receiver_external_id"` + + // Who triggered the notification (optional, admin or system operation will not have this field) + TriggerUserID string `json:"trigger_user_id"` + // The trigger user's display name (optional, admin or system operation will not have this field) + TriggerUserDisplayName string `json:"trigger_user_display_name"` + // The trigger user's url (optional, admin or system operation will not have this field) + TriggerUserUrl string `json:"trigger_user_url"` + + // the question title + QuestionTitle string `json:"question_title"` + // the question url + QuestionUrl string `json:"question_url"` + // the question tags (comma separated, optional, only for new question notification) + QuestionTags string `json:"tags"` + + // the answer url (optional, only for new answer notification) + AnswerUrl string `json:"answer_url"` + // the comment url (optional, only for new comment notification) + CommentUrl string `json:"comment_url"` +} + +var ( + // CallNotification is a function that calls all registered notification plugins + CallNotification, + registerNotification = MakePlugin[Notification](false) +) diff --git a/plugin/parser.go b/plugin/parser.go new file mode 100644 index 000000000..a9c5672a0 --- /dev/null +++ b/plugin/parser.go @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type Parser interface { + Base + Parse(text string) (string, error) +} + +var ( + // CallParser is a function that calls all registered parsers + CallParser, + registerParser = MakePlugin[Parser](false) +) diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 000000000..a9e173100 --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "encoding/json" + "sync" + + "github.com/segmentfault/pacman/cache" + "github.com/segmentfault/pacman/i18n" + "xorm.io/xorm" + + "github.com/apache/answer/internal/base/handler" + "github.com/apache/answer/internal/base/translator" + "github.com/gin-gonic/gin" +) + +// Data is defined here to avoid circular dependency with internal/base/data +type Data struct { + DB *xorm.Engine + Cache cache.Cache +} + +// GinContext is a wrapper of gin.Context +// We export it to make it easy to use in plugins +type GinContext = gin.Context + +// StatusManager is a manager that manages the status of plugins +// Init Plugins: +// json.Unmarshal([]byte(`{"plugin1": true, "plugin2": false}`), &plugin.StatusManager) +// Dump Status: +// json.Marshal(plugin.StatusManager) +var StatusManager = statusManager{ + status: make(map[string]bool), +} + +// Register registers a plugin +func Register(p Base) { + registerBase(p) + + if _, ok := p.(Config); ok { + registerConfig(p.(Config)) + } + + if _, ok := p.(UserConfig); ok { + registerUserConfig(p.(UserConfig)) + } + + if _, ok := p.(Connector); ok { + registerConnector(p.(Connector)) + } + + if _, ok := p.(Parser); ok { + registerParser(p.(Parser)) + } + + if _, ok := p.(Filter); ok { + registerFilter(p.(Filter)) + } + + if _, ok := p.(Storage); ok { + registerStorage(p.(Storage)) + } + + if _, ok := p.(Cache); ok { + registerCache(p.(Cache)) + } + + if _, ok := p.(UserCenter); ok { + registerUserCenter(p.(UserCenter)) + } + + if _, ok := p.(Agent); ok { + registerAgent(p.(Agent)) + } + + if _, ok := p.(Search); ok { + registerSearch(p.(Search)) + } + + if _, ok := p.(Notification); ok { + registerNotification(p.(Notification)) + } + + if _, ok := p.(Reviewer); ok { + registerReviewer(p.(Reviewer)) + } + + if _, ok := p.(Captcha); ok { + registerCaptcha(p.(Captcha)) + } + + if _, ok := p.(Embed); ok { + registerEmbed(p.(Embed)) + } + + if _, ok := p.(Render); ok { + registerRender(p.(Render)) + } + + if _, ok := p.(CDN); ok { + registerCDN(p.(CDN)) + } + + if _, ok := p.(Importer); ok { + registerImporter(p.(Importer)) + } + + if _, ok := p.(KVStorage); ok { + registerKVStorage(p.(KVStorage)) + } +} + +type Stack[T Base] struct { + plugins []T +} + +type RegisterFn[T Base] func(p T) +type Caller[T Base] func(p T) error +type CallFn[T Base] func(fn Caller[T]) error + +// MakePlugin creates a plugin caller and register stack manager +// The parameter super presents if the plugin can be disabled. +// It returns a register function and a caller function +// The register function is used to register a plugin, it will be called in the plugin's init function +// The caller function is used to call all registered plugins +func MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) { + stack := Stack[T]{} + + call := func(fn Caller[T]) error { + for _, p := range stack.plugins { + // If the plugin is disabled, skip it + if !super && !StatusManager.IsEnabled(p.Info().SlugName) { + continue + } + + if err := fn(p); err != nil { + return err + } + } + return nil + } + + register := func(p T) { + for _, plugin := range stack.plugins { + if plugin.Info().SlugName == p.Info().SlugName { + panic("plugin " + p.Info().SlugName + " is already registered") + } + } + stack.plugins = append(stack.plugins, p) + } + + return call, register +} + +type statusManager struct { + lock sync.Mutex + status map[string]bool +} + +func (m *statusManager) Enable(name string, enabled bool) { + m.lock.Lock() + defer m.lock.Unlock() + if !enabled { + m.status[name] = enabled + return + } + m.status[name] = enabled + + for _, slugName := range coordinatedCaptchaPlugins(name) { + m.status[slugName] = false + } + + for _, slugName := range coordinatedCDNPlugins(name) { + m.status[slugName] = false + } +} + +func (m *statusManager) IsEnabled(name string) bool { + if status, ok := m.status[name]; ok { + return status + } + return false +} + +// MarshalJSON implements the json.Marshaler interface. +func (m *statusManager) MarshalJSON() ([]byte, error) { + return json.Marshal(m.status) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (m *statusManager) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.status) +} + +// Translate translates the key to the current language of the context +func Translate(ctx *GinContext, key string) string { + return translator.Tr(handler.GetLang(ctx), key) +} + +// TranslateWithData translates the key to the language with data +func TranslateWithData(lang i18n.Language, key string, data any) string { + return translator.TrWithData(lang, key, data) +} + +// TranslateFn presents a generator of translated string. +// We use it to delegate the translation work outside the plugin. +type TranslateFn func(ctx *GinContext) string + +// Translator contains a function that translates the key to the current language of the context +type Translator struct { + Fn TranslateFn +} + +// MakeTranslator generates a translator from the key +func MakeTranslator(key string) Translator { + t := func(ctx *GinContext) string { + return Translate(ctx, key) + } + return Translator{Fn: t} +} + +// Translate translates the key to the current language of the context +func (t Translator) Translate(ctx *GinContext) string { + if t.Fn == nil { + return "" + } + return t.Fn(ctx) +} diff --git a/plugin/plugin_test/kv_storage_test.go b/plugin/plugin_test/kv_storage_test.go new file mode 100644 index 000000000..0dcd5b46a --- /dev/null +++ b/plugin/plugin_test/kv_storage_test.go @@ -0,0 +1,412 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin_test + +import ( + "context" + "fmt" + "math/rand" + "sync" + "testing" + "time" + + "github.com/apache/answer/plugin" + "github.com/segmentfault/pacman/log" + _ "modernc.org/sqlite" +) + +var ( + testPlugin *TestKVStoragePlugin +) + +// Helper functions for testing +func mustSet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, value string) { + if err := kv.Set(ctx, plugin.KVParams{Group: group, Key: key, Value: value}); err != nil { + t.Fatalf("Failed to set %s/%s: %v", group, key, err) + } +} + +func mustGet(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key, expected string) { + val, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key}) + if err != nil { + t.Fatalf("Failed to get %s/%s: %v", group, key, err) + } + if val != expected { + t.Errorf("Expected '%s' for %s/%s, got '%s'", expected, group, key, val) + } +} + +func mustDel(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) { + if err := kv.Del(ctx, plugin.KVParams{Group: group, Key: key}); err != nil { + t.Fatalf("Failed to delete %s/%s: %v", group, key, err) + } +} + +func assertNotFound(t *testing.T, kv *plugin.KVOperator, ctx context.Context, group, key string) { + val, err := kv.Get(ctx, plugin.KVParams{Group: group, Key: key}) + if err != plugin.ErrKVKeyNotFound { + t.Errorf("Expected ErrKVKeyNotFound for %s/%s, got: %v", group, key, err) + } + if val != "" { + t.Errorf("Expected empty value for %s/%s, got: '%s'", group, key, val) + } +} + +func assertError(t *testing.T, err error, expected error, msg string) { + if err != expected { + t.Errorf("%s: expected %v, got %v", msg, expected, err) + } +} + +// TestKVStoragePlugin implements KVStorage interface for testing +type TestKVStoragePlugin struct { + operator *plugin.KVOperator +} + +// Info returns plugin information +func (p *TestKVStoragePlugin) Info() plugin.Info { + return plugin.Info{ + Name: plugin.MakeTranslator("test_kv_storage_name"), + SlugName: "test_kv_storage", + Description: plugin.MakeTranslator("test_kv_storage_desc"), + Author: "Answer Team", + Version: "1.0.0", + Link: "https://github.com/apache/answer", + } +} + +// SetOperator sets KV operator +func (p *TestKVStoragePlugin) SetOperator(operator *plugin.KVOperator) { + p.operator = operator +} + +// setupTestEnvironment sets up test environment +func setupTestEnvironment() { + // Initialize only once + if testPlugin != nil { + return + } + + // Create and register test plugin + testPlugin = &TestKVStoragePlugin{} + plugin.Register(testPlugin) + + // Enable plugin + plugin.StatusManager.Enable("test_kv_storage", true) + + // Initialize plugin data, refer to plugin_common_service.go implementation + _ = plugin.CallKVStorage(func(k plugin.KVStorage) error { + k.SetOperator(plugin.NewKVOperator( + testDataSource.DB, + testDataSource.Cache, + k.Info().SlugName, + )) + return nil + }) +} + +// Test basic operations including CRUD and edge cases +func TestBasicOperations(t *testing.T) { + setupTestEnvironment() + kv := testPlugin.operator + ctx := context.Background() + + t.Run("BasicCRUD", func(t *testing.T) { + // Set/Get + mustSet(t, kv, ctx, "group1", "key1", "value1") + mustGet(t, kv, ctx, "group1", "key1", "value1") + + // Update + mustSet(t, kv, ctx, "group1", "key1", "new_value") + mustGet(t, kv, ctx, "group1", "key1", "new_value") + + // Delete + mustDel(t, kv, ctx, "group1", "key1") + assertNotFound(t, kv, ctx, "group1", "key1") + + // Group operation + mustSet(t, kv, ctx, "group1", "key2", "value2") + mustSet(t, kv, ctx, "group1", "key3", "value3") + groupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "group1", Page: 1, PageSize: 10}) + if err != nil { + t.Fatalf("Failed to get group data: %v", err) + } + + // the groupData should only have key2 and key3 because key1 is deleted + if len(groupData) != 2 { + t.Errorf("Expected 2 items, got %d", len(groupData)) + } + if groupData["key2"] != "value2" || groupData["key3"] != "value3" { + t.Errorf("Unexpected group data: %v", groupData) + } + }) + + t.Run("EdgeCases", func(t *testing.T) { + // Empty key + err := kv.Set(ctx, plugin.KVParams{Group: "group", Key: "", Value: "value"}) + assertError(t, err, plugin.ErrKVKeyEmpty, "Empty key test") + + // Empty group query + _, err = kv.GetByGroup(ctx, plugin.KVParams{Group: "", Page: 1, PageSize: 10}) + assertError(t, err, plugin.ErrKVGroupEmpty, "Empty group test") + + // Non-existent key + assertNotFound(t, kv, ctx, "non_exist_group", "non_exist_key") + + // Cache penetration protection + key := fmt.Sprintf("non_exist_key_%d", time.Now().UnixNano()) + assertNotFound(t, kv, ctx, "cache_penetration", key) + }) + + t.Run("CacheConsistency", func(t *testing.T) { + mustSet(t, kv, ctx, "cache_group", "cache_key", "cache_value") + mustGet(t, kv, ctx, "cache_group", "cache_key", "cache_value") + + // Update and verify immediate consistency + mustSet(t, kv, ctx, "cache_group", "cache_key", "updated_value") + mustGet(t, kv, ctx, "cache_group", "cache_key", "updated_value") + }) +} + +// Test transactions including rollback and nested transactions +func TestTransactions(t *testing.T) { + setupTestEnvironment() + kv := testPlugin.operator + ctx := context.Background() + + t.Run("SuccessfulTransaction", func(t *testing.T) { + err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { + if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key1", Value: "tx_value1"}); err != nil { + return err + } + if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key2", Value: "tx_value2"}); err != nil { + return err + } + return nil + }) + if err != nil { + t.Fatalf("Successful transaction failed: %v", err) + } + + mustGet(t, kv, ctx, "tx_group", "tx_key1", "tx_value1") + mustGet(t, kv, ctx, "tx_group", "tx_key2", "tx_value2") + }) + + t.Run("TransactionRollback", func(t *testing.T) { + err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { + if err := txKv.Set(ctx, plugin.KVParams{Group: "tx_group", Key: "tx_key3", Value: "tx_value3"}); err != nil { + return err + } + return fmt.Errorf("mock error") + }) + if err == nil { + t.Error("Expected transaction to fail but it succeeded") + } + + assertNotFound(t, kv, ctx, "tx_group", "tx_key3") + }) + + t.Run("NestedTransactions", func(t *testing.T) { + err := kv.Tx(ctx, func(ctx context.Context, txKv *plugin.KVOperator) error { + if err := txKv.Set(ctx, plugin.KVParams{Group: "nested", Key: "key1", Value: "value1"}); err != nil { + return err + } + + return txKv.Tx(ctx, func(ctx context.Context, nestedKv *plugin.KVOperator) error { + if err := nestedKv.Set(ctx, plugin.KVParams{Group: "nested", Key: "key2", Value: "value2"}); err != nil { + return err + } + return fmt.Errorf("mock nested error") + }) + }) + if err == nil { + t.Error("Expected nested transaction to fail but it succeeded") + } + + // Verify outer transaction also rolled back + assertNotFound(t, kv, ctx, "nested", "key1") + assertNotFound(t, kv, ctx, "nested", "key2") + }) +} + +// Test pagination in GetByGroup +func TestPagination(t *testing.T) { + setupTestEnvironment() + kv := testPlugin.operator + ctx := context.Background() + totalItems := 25 + + for i := range totalItems { + mustSet(t, kv, ctx, "pagination", fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)) + } + + // Test pagination + page1, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 1, PageSize: 10}) + if err != nil { + t.Fatalf("Failed to get page 1: %v", err) + } + if len(page1) != 10 { + t.Errorf("Page 1: expected 10 items, got %d", len(page1)) + } + + page2, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 2, PageSize: 10}) + if err != nil { + t.Fatalf("Failed to get page 2: %v", err) + } + if len(page2) != 10 { + t.Errorf("Page 2: expected 10 items, got %d", len(page2)) + } + + page3, err := kv.GetByGroup(ctx, plugin.KVParams{Group: "pagination", Page: 3, PageSize: 10}) + if err != nil { + t.Fatalf("Failed to get page 3: %v", err) + } + if len(page3) != 5 { + t.Errorf("Page 3: expected 5 items, got %d", len(page3)) + } + + // Verify different keys on different pages + for i := range 10 { + key := fmt.Sprintf("key%d", i) + if _, ok := page1[key]; !ok { + t.Errorf("Pagination test failed, key %s should be on page 1", key) + } + } + for i := range 10 { + key := fmt.Sprintf("key%d", i+10) + if _, ok := page2[key]; !ok { + t.Errorf("Pagination test failed, key %s should be on page 2", key) + } + } +} + +// Test concurrent operations and performance +func TestConcurrency(t *testing.T) { + setupTestEnvironment() + kv := testPlugin.operator + ctx := context.Background() + + t.Run("BasicConcurrency", func(t *testing.T) { + parallel := 10 + var wg sync.WaitGroup + wg.Add(parallel) + + for i := range parallel { + go func(index int) { + defer wg.Done() + time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) + mustSet(t, kv, ctx, "concurrent", fmt.Sprintf("key%d", index), "value") + }(i) + } + wg.Wait() + + // Verify results + wg.Add(parallel) + for i := range parallel { + go func(index int) { + defer wg.Done() + time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond) + mustGet(t, kv, ctx, "concurrent", fmt.Sprintf("key%d", index), "value") + }(i) + } + wg.Wait() + }) + + t.Run("StressTest", func(t *testing.T) { + if testing.Short() { + t.Skip("Skipping stress test in short mode") + } + + totalOps := 1000 + workerCount := 20 + prefix := "stress_test" + opsPerWorker := totalOps / workerCount + + log.Info("Starting KV storage stress test...") + startTime := time.Now() + + // Concurrent write test + var wg sync.WaitGroup + errorCount := int64(0) + + for w := range workerCount { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + startIdx := workerID * opsPerWorker + + for i := range opsPerWorker { + i := startIdx + i + err := kv.Set(ctx, plugin.KVParams{ + Group: prefix, + Key: fmt.Sprintf("key%d", i), + Value: fmt.Sprintf("value%d", i), + }) + if err != nil { + log.Warnf("Write error: %v", err) + errorCount++ + } + } + }(w) + } + wg.Wait() + + writeTime := time.Since(startTime) + + // Verify data integrity + groupData, err := kv.GetByGroup(ctx, plugin.KVParams{Group: prefix, Page: 1, PageSize: totalOps}) + if err != nil { + t.Fatalf("Failed to verify data: %v", err) + } + if len(groupData) != totalOps { + t.Errorf("Data loss: expected %d items, got %d", totalOps, len(groupData)) + } + + // Concurrent read test + startTime = time.Now() + readErrors := int64(0) + + wg.Add(workerCount) + for range workerCount { + go func() { + defer wg.Done() + for range opsPerWorker { + keyIdx := rand.Intn(totalOps) + key := fmt.Sprintf("key%d", keyIdx) + expected := fmt.Sprintf("value%d", keyIdx) + + val, err := kv.Get(ctx, plugin.KVParams{Group: prefix, Key: key}) + if err != nil { + readErrors++ + } else if val != expected { + t.Errorf("Data inconsistency: key=%s, expected=%s, got=%s", key, expected, val) + } + } + }() + } + wg.Wait() + + readTime := time.Since(startTime) + + log.Infof("Stress test completed:") + log.Infof(" Write: %d ops in %v (%.1f ops/sec), %d errors", totalOps, writeTime, float64(totalOps)/writeTime.Seconds(), errorCount) + log.Infof(" Read: %d ops in %v (%.1f ops/sec), %d errors", totalOps, readTime, float64(totalOps)/readTime.Seconds(), readErrors) + }) +} diff --git a/plugin/plugin_test/plugin_main_test.go b/plugin/plugin_test/plugin_main_test.go new file mode 100644 index 000000000..add11460b --- /dev/null +++ b/plugin/plugin_test/plugin_main_test.go @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin_test + +import ( + "context" + "database/sql" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/apache/answer/internal/base/data" + "github.com/apache/answer/internal/migrations" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/segmentfault/pacman/cache" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +var ( + mysqlDBSetting = TestDBSetting{ + Driver: string(schemas.MYSQL), + ImageName: "mariadb", + ImageVersion: "10.4.7", + ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"}, + PortID: "3306/tcp", + Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id + } + postgresDBSetting = TestDBSetting{ + Driver: string(schemas.POSTGRES), + ImageName: "postgres", + ImageVersion: "14", + ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"}, + PortID: "5432/tcp", + Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable", + } + sqlite3DBSetting = TestDBSetting{ + Driver: string(schemas.SQLITE), + Connection: filepath.Join(os.TempDir(), "answer-test-data.db"), + } + dbSettingMapping = map[string]TestDBSetting{ + mysqlDBSetting.Driver: mysqlDBSetting, + sqlite3DBSetting.Driver: sqlite3DBSetting, + postgresDBSetting.Driver: postgresDBSetting, + } + // after all test down will execute tearDown function to clean-up + tearDown func() + // testDataSource used for repo testing + testDataSource *data.Data + testCache cache.Cache +) + +func TestMain(t *testing.M) { + dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")] + if !ok { + // Use sqlite3 to test. + dbSetting = dbSettingMapping[string(schemas.SQLITE)] + } + if dbSetting.Driver == string(schemas.SQLITE) { + os.RemoveAll(dbSetting.Connection) + } + + defer func() { + if tearDown != nil { + tearDown() + } + }() + if err := initTestDataSource(dbSetting); err != nil { + panic(err) + } + log.Info("init test database successfully") + + if ret := t.Run(); ret != 0 { + os.Exit(ret) + } +} + +type TestDBSetting struct { + Driver string + ImageName string + ImageVersion string + ENV []string + PortID string + Connection string +} + +func initTestDataSource(dbSetting TestDBSetting) error { + connection, imageCleanUp, err := initDatabaseImage(dbSetting) + if err != nil { + return err + } + dbSetting.Connection = connection + + dbEngine, err := initDatabase(dbSetting) + if err != nil { + return err + } + + newCache, err := initCache() + if err != nil { + return err + } + + newData, dbCleanUp, err := data.NewData(dbEngine, newCache) + if err != nil { + return err + } + testDataSource = newData + testCache = newCache + + tearDown = func() { + dbCleanUp() + log.Info("cleanup test database successfully") + imageCleanUp() + log.Info("cleanup test database image successfully") + } + return nil +} + +func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { + // sqlite3 don't need to set up image + if dbSetting.Driver == string(schemas.SQLITE) { + return dbSetting.Connection, func() { + log.Info("remove database", dbSetting.Connection) + err = os.Remove(dbSetting.Connection) + if err != nil { + log.Error(err) + } + }, nil + } + pool, err := dockertest.NewPool("") + pool.MaxWait = time.Minute * 5 + if err != nil { + return "", nil, fmt.Errorf("could not connect to docker: %s", err) + } + + //resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV) + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: dbSetting.ImageName, + Tag: dbSetting.ImageVersion, + Env: dbSetting.ENV, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + return "", nil, fmt.Errorf("could not pull resource: %s", err) + } + + connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) + if err := pool.Retry(func() error { + db, err := sql.Open(dbSetting.Driver, connection) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + return "", nil, fmt.Errorf("could not connect to database: %s", err) + } + return connection, func() { _ = pool.Purge(resource) }, nil +} + +func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { + dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} + dbEngine, err = data.NewDB(true, dataConf) + if err != nil { + return nil, fmt.Errorf("connection to database failed: %s", err) + } + if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{ + Language: "en_US", + SiteName: "ANSWER", + SiteURL: "http://127.0.0.1:8080/", + ContactEmail: "answer@answer.com", + AdminName: "admin", + AdminPassword: "admin", + AdminEmail: "answer@answer.com", + }).InitDB(); err != nil { + return nil, fmt.Errorf("migrations init database failed: %s", err) + } + return dbEngine, nil +} + +func initCache() (newCache cache.Cache, err error) { + newCache, _, err = data.NewCache(&data.CacheConf{}) + return newCache, err +} diff --git a/plugin/render.go b/plugin/render.go new file mode 100644 index 000000000..d36fd86d7 --- /dev/null +++ b/plugin/render.go @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import "github.com/gin-gonic/gin" + +type RenderConfig struct { + SelectTheme string `json:"select_theme"` +} + +// select_theme + +type Render interface { + Base + GetRenderConfig(ctx *gin.Context) (renderConfig *RenderConfig) +} + +var ( + // CallRender is a function that calls all registered parsers + CallRender, + registerRender = MakePlugin[Render](false) +) diff --git a/plugin/reviewer.go b/plugin/reviewer.go new file mode 100644 index 000000000..488c6f9ba --- /dev/null +++ b/plugin/reviewer.go @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type Reviewer interface { + Base + Review(content *ReviewContent) (result *ReviewResult) +} + +// ReviewContent is a struct that contains the content of a review +type ReviewContent struct { + // The type of the content, e.g. question, answer + ObjectType string + // The title of the content, only available for the question + Title string + // The content of the review, always available + Content string + // The tags of the content, only available for the question + Tags []string + // The author of the content + Author ReviewContentAuthor + // Review Language, the site language. e.g. en_US + // The plugin may reply the review result according to the language + Language string + // The user agent of the request web browser + UserAgent string + // The IP address of the request + IP string +} + +type ReviewContentAuthor struct { + // The user's reputation + Rank int + // The amount of questions that has approved + ApprovedQuestionAmount int64 + // The amount of answers that has approved + ApprovedAnswerAmount int64 + // 1:User 2:Admin 3:Moderator + Role int +} + +type ReviewStatus string + +const ( + ReviewStatusApproved ReviewStatus = "approved" + ReviewStatusDeleteDirectly ReviewStatus = "delete_directly" + ReviewStatusNeedReview ReviewStatus = "need_review" +) + +// ReviewResult is a struct that contains the result of a review +type ReviewResult struct { + // If the review is approved + Approved bool + // The status of the review + ReviewStatus ReviewStatus + // The reason for the result + Reason string +} + +var ( + // CallReviewer is a function that calls all registered parsers + CallReviewer, + registerReviewer = MakePlugin[Reviewer](false) +) diff --git a/plugin/search.go b/plugin/search.go new file mode 100644 index 000000000..02b5fba8f --- /dev/null +++ b/plugin/search.go @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +import ( + "context" +) + +type SearchResult struct { + // ID content ID + ID string + // Type content type, example: "answer", "question" + Type string +} + +type SearchContent struct { + ObjectID string `json:"objectID"` + Title string `json:"title"` + Type string `json:"type"` + Content string `json:"content"` + Answers int64 `json:"answers"` + Status SearchContentStatus `json:"status"` + Tags []string `json:"tags"` + QuestionID string `json:"questionID"` + UserID string `json:"userID"` + Views int64 `json:"views"` + Created int64 `json:"created"` + Active int64 `json:"active"` + Score int64 `json:"score"` + HasAccepted bool `json:"hasAccepted"` +} + +type SearchBasicCond struct { + // From zero-based page number + Page int + // Page size + PageSize int + + // The keywords for search. + Words []string + // TagIDs is a list of tag IDs. + TagIDs [][]string + // The object's owner user ID. + UserID string + // The order of the search result. + Order SearchOrderCond + + // Weathers the question is accepted or not. Only support search question. + QuestionAccepted SearchAcceptedCond + // Weathers the answer is accepted or not. Only support search answer. + AnswerAccepted SearchAcceptedCond + + // Only support search answer. + QuestionID string + + // greater than or equal to the number of votes. + VoteAmount int + // greater than or equal to the number of views. + ViewAmount int + // greater than or equal to the number of answers. Only support search question. + AnswerAmount int +} + +type SearchAcceptedCond int +type SearchContentStatus int +type SearchOrderCond string + +const ( + AcceptedCondAll SearchAcceptedCond = iota + AcceptedCondTrue + AcceptedCondFalse +) + +const ( + SearchContentStatusAvailable = 1 + SearchContentStatusDeleted = 10 +) + +const ( + SearchNewestOrder SearchOrderCond = "newest" + SearchActiveOrder SearchOrderCond = "active" + SearchScoreOrder SearchOrderCond = "score" + SearchRelevanceOrder SearchOrderCond = "relevance" +) + +type Search interface { + Base + Description() SearchDesc + RegisterSyncer(ctx context.Context, syncer SearchSyncer) + SearchContents(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + SearchQuestions(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + SearchAnswers(ctx context.Context, cond *SearchBasicCond) (res []SearchResult, total int64, err error) + UpdateContent(ctx context.Context, content *SearchContent) (err error) + DeleteContent(ctx context.Context, objectID string) (err error) +} + +type SearchDesc struct { + // A svg icon it wil be display in search result page. optional + Icon string `json:"icon"` + // The link address of the search engine. optional + Link string `json:"link"` +} + +type SearchSyncer interface { + GetAnswersPage(ctx context.Context, page, pageSize int) (answerList []*SearchContent, err error) + GetQuestionsPage(ctx context.Context, page, pageSize int) (questionList []*SearchContent, err error) +} + +var ( + // CallSearch is a function that calls all registered parsers + CallSearch, + registerSearch = MakePlugin[Search](false) +) diff --git a/plugin/storage.go b/plugin/storage.go new file mode 100644 index 000000000..599a41c4c --- /dev/null +++ b/plugin/storage.go @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type UploadSource string + +const ( + UserAvatar UploadSource = "user_avatar" + UserPost UploadSource = "user_post" + UserPostAttachment UploadSource = "user_post_attachment" + AdminBranding UploadSource = "admin_branding" +) + +var ( + DefaultFileTypeCheckMapping = map[UploadSource]map[string]bool{ + UserAvatar: { + ".jpg": true, + ".jpeg": true, + ".png": true, + ".webp": true, + }, + UserPost: { + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".webp": true, + }, + AdminBranding: { + ".jpg": true, + ".jpeg": true, + ".png": true, + ".ico": true, + }, + } +) + +type UploadFileCondition struct { + // Source is the source of the file + Source UploadSource + // MaxImageSize is the maximum size of the image in MB + MaxImageSize int + // MaxAttachmentSize is the maximum size of the attachment in MB + MaxAttachmentSize int + // MaxImageMegapixel is the maximum megapixel of the image + MaxImageMegapixel int + // AuthorizedImageExtensions is the list of authorized image extensions + AuthorizedImageExtensions []string + // AuthorizedAttachmentExtensions is the list of authorized attachment extensions + AuthorizedAttachmentExtensions []string +} + +type UploadFileResponse struct { + // FullURL is the URL that can be used to access the file + FullURL string + // OriginalError is the error returned by the storage plugin. It is used for debugging. + OriginalError error + // DisplayErrorMsg is the error message that will be displayed to the user. + DisplayErrorMsg Translator +} + +type Storage interface { + Base + + // UploadFile uploads a file to storage. + // The file is in the Form of the ctx and the key is "file" + UploadFile(ctx *GinContext, condition UploadFileCondition) UploadFileResponse +} + +var ( + // CallStorage is a function that calls all registered storage + CallStorage, + registerStorage = MakePlugin[Storage](false) +) diff --git a/plugin/user_center.go b/plugin/user_center.go new file mode 100644 index 000000000..a47676560 --- /dev/null +++ b/plugin/user_center.go @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type UserCenter interface { + Base + // Description returns the description of the user center, including the name, icon, url, etc. + Description() UserCenterDesc + // ControlCenterItems returns the items that will be displayed in the control center + ControlCenterItems() []ControlCenter + // LoginCallback is called when the user center login callback is called + LoginCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error) + // SignUpCallback is called when the user center sign up callback is called + SignUpCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error) + // UserInfo returns the user information + UserInfo(externalID string) (userInfo *UserCenterBasicUserInfo, err error) + // UserStatus returns the latest user status + UserStatus(externalID string) (userStatus UserStatus) + // UserList returns the user list information + UserList(externalIDs []string) (userInfo []*UserCenterBasicUserInfo, err error) + // UserSettings returns the user settings + UserSettings(externalID string) (userSettings *SettingInfo, err error) + // PersonalBranding returns the personal branding information + PersonalBranding(externalID string) (branding []*PersonalBranding) + // AfterLogin is called after the user logs in + AfterLogin(externalID, accessToken string) +} + +type UserCenterDesc struct { + Name string `json:"name"` + DisplayName Translator `json:"display_name"` + Icon string `json:"icon"` + Url string `json:"url"` + LoginRedirectURL string `json:"login_redirect_url"` + SignUpRedirectURL string `json:"sign_up_redirect_url"` + RankAgentEnabled bool `json:"rank_agent_enabled"` + UserStatusAgentEnabled bool `json:"user_status_agent_enabled"` + UserRoleAgentEnabled bool `json:"user_role_agent_enabled"` + MustAuthEmailEnabled bool `json:"must_auth_email_enabled"` + EnabledOriginalUserSystem bool `json:"enabled_original_user_system"` +} + +type UserStatus int + +const ( + UserStatusAvailable UserStatus = 1 + UserStatusSuspended UserStatus = 9 + UserStatusDeleted UserStatus = 10 +) + +type UserCenterBasicUserInfo struct { + ExternalID string `json:"external_id"` + Username string `json:"username"` + DisplayName string `json:"display_name"` + Email string `json:"email"` + Rank int `json:"rank"` + Avatar string `json:"avatar"` + Mobile string `json:"mobile"` + Bio string `json:"bio"` + Status UserStatus `json:"status"` +} + +type ControlCenter struct { + Name string `json:"name"` + Label string `json:"label"` + Url string `json:"url"` +} + +type SettingInfo struct { + ProfileSettingRedirectURL string `json:"profile_setting_redirect_url"` + AccountSettingRedirectURL string `json:"account_setting_redirect_url"` +} + +type PersonalBranding struct { + Icon string `json:"icon"` + Name string `json:"name"` + Label string `json:"label"` + Url string `json:"url"` +} + +var ( + // CallUserCenter is a function that calls all registered parsers + CallUserCenter, + registerUserCenter = MakePlugin[UserCenter](false) +) + +func UserCenterEnabled() (enabled bool) { + _ = CallUserCenter(func(fn UserCenter) error { + enabled = true + return nil + }) + return +} + +func RankAgentEnabled() (enabled bool) { + _ = CallUserCenter(func(fn UserCenter) error { + enabled = fn.Description().RankAgentEnabled + return nil + }) + return +} + +func GetUserCenter() (uc UserCenter, ok bool) { + _ = CallUserCenter(func(fn UserCenter) error { + uc = fn + ok = true + return nil + }) + return +} diff --git a/plugin/user_config.go b/plugin/user_config.go new file mode 100644 index 000000000..07070d84e --- /dev/null +++ b/plugin/user_config.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package plugin + +type UserConfig interface { + Base + + // UserConfigFields returns the list of config fields + UserConfigFields() []ConfigField + // UserConfigReceiver receives the config data, it calls when the config is saved or initialized. + // We recommend to unmarshal the data to a struct, and then use the struct to do something. + // The config is encoded in JSON format. + // It depends on the definition of ConfigFields. + UserConfigReceiver(userID string, config []byte) error +} + +var ( + // CallUserConfig is a function that calls all registered config plugins + CallUserConfig, + registerUserConfig = MakePlugin[UserConfig](false) + getPluginUserConfigFn func(userID, pluginSlugName string) []byte +) + +// GetPluginUserConfig returns the user config of the given user id +func GetPluginUserConfig(userID, pluginSlugName string) []byte { + if getPluginUserConfigFn != nil { + return getPluginUserConfigFn(userID, pluginSlugName) + } + return nil +} + +// RegisterGetPluginUserConfigFunc registers a function to get the user config of the given user id +func RegisterGetPluginUserConfigFunc(fn func(userID, pluginSlugName string) []byte) { + getPluginUserConfigFn = fn +} diff --git a/script/build_plugin.sh b/script/build_plugin.sh new file mode 100755 index 000000000..f53cb770b --- /dev/null +++ b/script/build_plugin.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -e +echo "begin build plugin" +plugin_file=./script/plugin_list +if [ ! -f "$plugin_file" ]; then + echo "plugin_list is not exist" + exit 0 +fi + +echo "plugin_list exist" +cmd="./answer build " +for repo in `cat $plugin_file` +do + echo ${repo} + cmd=$cmd" --with "${repo} +done + +echo "cmd is "$cmd +$cmd +if [ ! -f "./new_answer" ]; then + echo "new_answer is not exist build failed" + exit 1 +fi +rm answer +mv new_answer answer +./answer plugin \ No newline at end of file diff --git a/script/check-asf-header.sh b/script/check-asf-header.sh new file mode 100755 index 000000000..808efa108 --- /dev/null +++ b/script/check-asf-header.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# check if docker or podman is installed +if command -v docker >/dev/null 2>&1; then + CONTAINER_RUNTIME="docker" +elif command -v podman >/dev/null 2>&1; then + CONTAINER_RUNTIME="podman" +else + echo "Neither Docker nor Podman is installed. Please install either Docker or Podman." + exit 1 +fi + +$CONTAINER_RUNTIME run -it --rm -v "$(pwd)":/github/workspace ghcr.io/korandoru/hawkeye-native format + +gofmt -w -l . diff --git a/script/entrypoint.sh b/script/entrypoint.sh index c66528d8a..3b3410788 100755 --- a/script/entrypoint.sh +++ b/script/entrypoint.sh @@ -1,3 +1,21 @@ #!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + /usr/bin/answer init -/usr/bin/answer run -c /data/conf/config.yaml +/usr/bin/answer upgrade +/usr/bin/answer run -C /data/ diff --git a/script/gen-api.sh b/script/gen-api.sh index f76b6f7ae..c5026824b 100755 --- a/script/gen-api.sh +++ b/script/gen-api.sh @@ -1,3 +1,20 @@ #!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + cd ../ swag init --generalInfo ./cmd/answer/main.go diff --git a/script/plugin_list b/script/plugin_list new file mode 100644 index 000000000..8a7676276 --- /dev/null +++ b/script/plugin_list @@ -0,0 +1,3 @@ +github.com/apache/answer-plugins/connector-basic@latest +github.com/apache/answer-plugins/reviewer-basic@latest +github.com/apache/answer-plugins/captcha-basic@latest \ No newline at end of file diff --git a/ui/.editorconfig b/ui/.editorconfig index ff673e092..fd0ff18ab 100644 --- a/ui/.editorconfig +++ b/ui/.editorconfig @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + # http://editorconfig.org root = true diff --git a/ui/.env b/ui/.env deleted file mode 100644 index 6893217c2..000000000 --- a/ui/.env +++ /dev/null @@ -1 +0,0 @@ -PUBLIC_URL= diff --git a/ui/.env.development b/ui/.env.development index 8648d82a1..a634cee2e 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1 +1,2 @@ -REACT_APP_API_URL=http://10.0.10.98:2060 +PUBLIC_URL +REACT_APP_API_URL = http://10.0.20.84:8080/ diff --git a/ui/.env.production b/ui/.env.production index 940bd4a8f..c371a163b 100644 --- a/ui/.env.production +++ b/ui/.env.production @@ -1,3 +1,5 @@ +TSC_COMPILE_ON_ERROR=true +ESLINT_NO_DEV_ERRORS=true +PUBLIC_URL=/ REACT_APP_API_URL=/ -REACT_APP_PUBLIC_PATH=/ -REACT_APP_VERSION= +REACT_APP_BASE_URL= diff --git a/ui/.env.test b/ui/.env.test deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/.eslintignore b/ui/.eslintignore index a93b8236a..1e6d1c5cd 100644 --- a/ui/.eslintignore +++ b/ui/.eslintignore @@ -5,3 +5,6 @@ build .eslintrc.js node_modules/ src/types/ +scripts/ +src/plugins/** +!src/plugins/builtin diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 877a00391..1d9052600 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,11 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + module.exports = { + root: true, env: { browser: true, es2021: true, }, extends: [ - 'react-app', 'react-app/jest', + 'plugin:react/recommended', 'airbnb', 'airbnb-typescript', 'plugin:import/typescript', @@ -19,10 +39,12 @@ module.exports = { }, ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], }, - plugins: ['react', '@typescript-eslint'], + plugins: ['react', '@typescript-eslint', 'prettier'], rules: { + 'prettier/prettier': 'error', 'no-unused-vars': 'off', 'no-console': 'off', 'import/prefer-default-export': 'off', @@ -33,11 +55,13 @@ module.exports = { 'react/no-unescaped-entities': 'off', 'react/require-default-props': 'off', 'arrow-body-style': 'off', + "global-require": "off", 'react/prop-types': 0, 'react/no-danger': 'off', 'jsx-a11y/no-static-element-interactions': 'off', 'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/tabindex-no-positive': 'off', + 'jsx-a11y/control-has-associated-label': 'off', 'func-names': 'off', 'no-alert': 'off', 'prefer-promise-reject-errors': 'off', @@ -48,6 +72,8 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', 'react/jsx-props-no-spreading': 'off', '@typescript-eslint/default-param-last': 'off', + 'no-nested-ternary': 'off', + 'class-methods-use-this': 'off', 'import/order': [ 'error', { @@ -64,7 +90,7 @@ module.exports = { position: 'before', }, { - pattern: '@answer/**', + pattern: '@/**', group: 'internal', }, { @@ -83,5 +109,7 @@ module.exports = { 'newlines-between': 'always', }, ], + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/no-noninteractive-tabindex': 'off', }, }; diff --git a/ui/.gitignore b/ui/.gitignore index c4796f76e..a73983ee7 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -9,14 +9,15 @@ /coverage # production -/build + +/build/*/*/* +/build/*.json +/build/*.html +/build/*.txt # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +.env*.local npm-debug.log* yarn-debug.log* @@ -30,3 +31,11 @@ yarn.lock package-lock.json .eslintcache /.vscode/ + +/* !/src/plugins +/src/plugins/* +!/src/plugins/builtin +!/src/plugins/Demo +!/src/plugins/answer-chart +!/src/plugins/answer-formula +/src/plugins/*/*.go diff --git a/ui/.gitlab-ci.yml b/ui/.gitlab-ci.yml deleted file mode 100644 index df03dd154..000000000 --- a/ui/.gitlab-ci.yml +++ /dev/null @@ -1,93 +0,0 @@ -include: - - project: 'segmentfault/devops/templates' - file: - - .deploy-cdn.yml - - .deploy-helm.yml - -variables: - FF_USE_FASTZIP: 'true' - PROJECT_NAME: 'answer_static' - -stages: - - install - - publish - - deploy - -# 静态资源构建 -install: - image: dockerhub.qingcloud.com/sf_base/node-build:14 - stage: install - allow_failure: false - - cache: - - key: - files: - - pnpm-lock.yml - paths: - - node_modules/ - policy: pull-push - script: - - pnpm install - - if [ "$CI_COMMIT_BRANCH" = "dev" ]; then - sed -i "s//$PROJECT_NAME/g" .env.development; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.development; - pnpm run build:dev; - elif [ "$CI_COMMIT_BRANCH" = "main" ]; then - sed -i "s//$PROJECT_NAME/g" .env.test; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.test; - pnpm run build:test; - elif [ "$CI_COMMIT_BRANCH" = "release" ]; then - sed -i "s//$PROJECT_NAME/g" .env.production; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.production; - pnpm run build:prod; - fi - artifacts: - paths: - - build/ - -publish:cdn:dev: - extends: .deploy-cdn - stage: publish - only: - - dev - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: dev - -publish:cdn:test: - extends: .deploy-cdn - stage: publish - only: - - main - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: test - -publish:cdn:prod: - extends: .deploy-cdn - stage: publish - only: - - release - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: prod - -deploy:dev: - extends: .deploy-helm - stage: deploy - only: - - dev - needs: - - publish:cdn:dev - variables: - KubernetesCluster: dev - KubernetesNamespace: 'sf-test' - DockerTag: $CI_COMMIT_SHORT_SHA - ChartName: answer-web - InstallPolicy: replace diff --git a/ui/.lintstagedrc.json b/ui/.lintstagedrc.json index 8f3e37e62..9eb3c4458 100644 --- a/ui/.lintstagedrc.json +++ b/ui/.lintstagedrc.json @@ -1,5 +1,9 @@ { - "*.{js,jsx,ts,tsx}": ["eslint --cache --fix"], - "*.{js,jsx,less,md,json}": ["prettier --write"], - "*.ts?(x)": ["prettier --parser=typescript --write"] -} + "src/**/*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "src/**/*.{scss,md}": [ + "prettier --write" + ] +} \ No newline at end of file diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 000000000..885ffc35a --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1,2 @@ +strict-peer-dependencies = true +auto-install-peers = true diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json index 83fdbcaad..37d1a10df 100644 --- a/ui/.prettierrc.json +++ b/ui/.prettierrc.json @@ -3,5 +3,7 @@ "tabWidth": 2, "singleQuote": true, "jsxBracketSameLine": true, - "printWidth": 80 + "printWidth": 80, + "endOfLine": "auto", + "bracketSameLine": true } diff --git a/ui/README.md b/ui/README.md deleted file mode 100644 index 50bb1baa9..000000000 --- a/ui/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Answer - -`Answer` is a modern Q&A community application ✨ - -To learn more about the philosophy and goals of the project, visit [Answer](https://answer.dev.segmentfault.com). - -### 📦 Prerequisites - -- [Node.js](https://nodejs.org/) `>=16.17` -- [pnpm](https://pnpm.io/) `>=7` - -pnpm is required by building the Answer project. To installing the pnpm tools with below commands: - -```bash -corepack enable -corepack prepare pnpm@v7.12.2 --activate -``` - -With Node.js v16.17 or newer, you may install the latest version of pnpm by just specifying the tag: - -```bash -corepack prepare pnpm@latest --activate -``` - -## 🔨 Development - -clone the repo locally and run following command in your terminal: - -```shell -$ git clone git@github.com:answerdev/answer.git answer -$ cd answer/ui -$ pnpm install -$ pnpm run start -``` - -now, your browser should already open automatically, and autoload `http://localhost:3000`. -you can also manually visit it. - -## 👷 Workflow - -when cloning repo, and run `pnpm install` to init dependencies. you can use project commands below: - -- `pnpm run start` run Answer web locally. -- `pnpm run build:dev` build code for environment `dev` -- `pnpm run build:test` build code for environment `test` -- `pnpm run build:prod` build code for environment `prod` -- `pnpm run lint` lint and fix the code style -- `pnpm run cz` run `git commit` by `commitizen` - -## 🖥 Environment Support - -| [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| last 2 versions | last 2 versions | last 2 versions | last 2 versions | - -## Build with - -- [React.js](https://reactjs.org/) - Our front end is a React.js app. -- [Bootstrap](https://getbootstrap.com/) - UI library. -- [React Bootstrap](https://react-bootstrap.github.io/) - UI Library(rebuilt for React.) -- [axios](https://github.com/axios/axios) - Request library -- [SWR](https://swr.bootcss.com/) - Request library -- [react-i18next](https://react.i18next.com/) - International library -- [zustand](https://github.com/pmndrs/zustand) - State-management library diff --git a/ui/build/favicon.ico b/ui/build/favicon.ico new file mode 100644 index 000000000..6ab1fbdad Binary files /dev/null and b/ui/build/favicon.ico differ diff --git a/ui/build/index.html b/ui/build/index.html deleted file mode 100644 index 1eec7ed2b..000000000 --- a/ui/build/index.html +++ /dev/null @@ -1 +0,0 @@ -Answer
\ No newline at end of file diff --git a/ui/commitlint.config.js b/ui/commitlint.config.js index 84dcb122a..83816fca1 100644 --- a/ui/commitlint.config.js +++ b/ui/commitlint.config.js @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + module.exports = { extends: ['@commitlint/config-conventional'], }; diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 438c1c29b..7d62b1d8e 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,37 +1,154 @@ -const path = require('path'); +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { + addWebpackModuleRule, + addWebpackAlias, + setWebpackOptimizationSplitChunks, + addWebpackPlugin, +} = require("customize-cra"); +const webpack = require('webpack'); + +const path = require("path"); +const i18nPath = path.resolve(__dirname, "../i18n"); module.exports = { - webpack: function (config, env) { - if (env === 'production') { - config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; + webpack: function(config, env) { + addWebpackAlias({ + "@": path.resolve(__dirname, "src"), + "@i18n": i18nPath, + buffer: 'buffer', + })(config); + + addWebpackModuleRule({ + test: /\.ya?ml$/, + use: "yaml-loader" + })(config); + + addWebpackPlugin( + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }) + )(config); + + setWebpackOptimizationSplitChunks({ + maxInitialRequests: 20, + minSize: 20 * 1024, + minChunks: 2, + cacheGroups: { + automaticNamePrefix: 'chunk', + mix1: { + test: (module, chunks) => { + return ( + module.resource && + (module.resource.includes('components') || + /\/node_modules\/react-bootstrap\//.test(module.resource)) + ); + }, + name: 'chunk-mix1', + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 14, + reuseExistingChunk: true, + minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, + chunks: 'initial', + }, + mix2: { + name: 'chunk-mix2', + test: /[\/]node_modules[\/](i18next|lodash|marked|next-share)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 13, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + mix3: { + name: 'chunk-mix3', + test: /[\/]node_modules[\/](@remix-run|@restart|axios|diff)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 12, + reuseExistingChunk: true, + minChunks: 1, + chunks: 'initial', + }, + codemirror: { + name: 'codemirror', + test: /[\/]node_modules[\/](\@codemirror)[\/]/, + priority: 10, + reuseExistingChunk: true, + minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, + chunks: 'initial', + enforce: true, + }, + lezer: { + name: 'lezer', + test: /[\/]node_modules[\/](\@lezer)[\/]/, + priority: 9, + reuseExistingChunk: true, + minChunks: process.env.NODE_ENV === 'production' ? 1 : 2, + chunks: 'initial', + enforce: true, + }, + reactDom: { + name: 'react-dom', + test: /[\/]node_modules[\/](react-dom)[\/]/, + filename: 'static/js/[name].[contenthash:8].chunk.js', + priority: 8, + reuseExistingChunk: true, + chunks: 'all', + enforce: true, + }, + nodesInitial: { + name: 'chunk-nodesInitial', + filename: 'static/js/[name].[contenthash:8].chunk.js', + test: /[\/]node_modules[\/]/, + priority: 1, + minChunks: 1, + chunks: 'initial', + reuseExistingChunk: true, + }, + }, + })(config); + + // add i18n dir to ModuleScopePlugin allowedPaths + const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); + if (moduleScopePlugin) { + moduleScopePlugin.allowedPaths.push(i18nPath); } - config.resolve.alias = { - ...config.resolve.alias, - '@': path.resolve(__dirname, 'src'), - '@answer/pages': path.resolve(__dirname, 'src/pages'), - '@answer/components': path.resolve(__dirname, 'src/components'), - '@answer/stores': path.resolve(__dirname, 'src/stores'), - '@answer/hooks': path.resolve(__dirname, 'src/hooks'), - '@answer/utils': path.resolve(__dirname, 'src/utils'), - '@answer/common': path.resolve(__dirname, 'src/common'), - '@answer/api': path.resolve(__dirname, 'src/services/api'), - }; return config; }, - - devServer: function (configFunction) { - return function (proxy, allowedHost) { + devServer: function(configFunction) { + return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); - config.proxy = { - '/answer': { - target: "http://10.0.20.84:8080", - // target: 'http://10.0.10.98:2060', + config.proxy = [ + { + context: ['/answer', '/installation'], + target: process.env.REACT_APP_API_URL, changeOrigin: true, secure: false, }, - }; + { + context: ['/custom.css'], + target: process.env.REACT_APP_API_URL, + } + ]; return config; }; - }, + } }; diff --git a/ui/package.json b/ui/package.json index 40d0bd4a3..b88fc12da 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,64 +5,59 @@ "homepage": "/", "scripts": { "start": "react-app-rewired start", - "build:dev": "env-cmd -f .env.development react-app-rewired build", - "build:test": "env-cmd -f .env.test react-app-rewired build", - "build:prod": "env-cmd -f .env.production react-app-rewired build", - "build": "env-cmd -f .env react-app-rewired build", - "test": "react-app-rewired test", - "eject": "react-scripts eject", + "build": "node ./scripts/env.js && react-app-rewired build", + "pre-install": "node ./scripts/importPlugins.js && pnpm install && node ./scripts/preinstall.js ", + "prepare": "pnpm build:packages", + "build:packages": "pnpm -r --filter=./src/plugins/* run build", + "clean": "rm -rf node_modules && rm -rf src/plugins/**/node_modules", + "analyze": "source-map-explorer 'build/static/js/*.js'", + "setup-lint": "node scripts/setup-eslint.js && cd .. && husky install", "lint": "eslint . --cache --fix --ext .ts,.tsx", - "prepare": "cd .. && husky install", - "cz": "cz", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" - }, - "config": { - "commitizen": { - "path": "ui/node_modules/cz-conventional-changelog" - } + "prettier": "prettier --write \"src/**/*.{ts,tsx,css,scss,md}\"", + "lint-staged": "lint-staged" }, "dependencies": { - "@testing-library/jest-dom": "^4.2.4", - "ahooks": "^3.7.0", - "axios": "^0.27.2", - "bootstrap": "^5.2.0", - "bootstrap-icons": "^1.9.1", + "@codemirror/lang-markdown": "^6.2.4", + "@codemirror/language-data": "^6.5.0", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.26.1", + "axios": "^1.7.7", + "bootstrap": "^5.3.2", + "bootstrap-icons": "^1.10.5", "classnames": "^2.3.1", - "codemirror": "5.65.0", + "codemirror": "^6.0.1", + "color": "^4.2.3", "copy-to-clipboard": "^3.3.2", "dayjs": "^1.11.5", - "highlight.js": "^11.6.0", + "diff": "^5.1.0", + "front-matter": "^4.0.2", "i18next": "^21.9.0", - "i18next-chained-backend": "^3.0.2", - "i18next-http-backend": "^1.4.1", - "i18next-localstorage-backend": "^3.1.3", - "katex": "^0.16.2", + "js-sha256": "0.11.0", "lodash": "^4.17.21", "marked": "^4.0.19", - "mermaid": "^9.1.7", "next-share": "^0.18.1", + "qrcode": "^1.5.1", "qs": "^6.11.0", "react": "^18.2.0", - "react-bootstrap": "^2.5.0", + "react-bootstrap": "^2.10.0", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", "react-i18next": "^11.18.3", - "react-router-dom": "^6.4.0", + "react-router-dom": "^7.0.2", + "semver": "^7.3.8", "swr": "^1.3.0", - "zustand": "^4.1.1" + "zustand": "^5.0.2" }, "devDependencies": { - "@babel/core": "^7.18.10", - "@babel/plugin-syntax-flow": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.14.9", "@commitlint/cli": "^17.0.3", - "@commitlint/config-conventional": "^17.0.3", + "@commitlint/config-conventional": "^17.2.0", "@fullhuman/postcss-purgecss": "^4.1.3", - "@popperjs/core": "^2.11.5", "@testing-library/dom": "^8.17.1", + "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", + "@types/color": "^3.0.3", + "@types/dompurify": "^2.4.0", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.184", "@types/marked": "^4.0.6", @@ -70,42 +65,39 @@ "@types/qs": "^6.9.7", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", - "@types/react-helmet": "^6.1.5", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.33.0", - "commitizen": "^4.2.5", - "conventional-changelog-cli": "^2.2.2", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "buffer": "6.0.3", "customize-cra": "^1.0.0", - "cz-conventional-changelog": "^3.3.0", - "env-cmd": "^10.1.0", - "eslint": "^8.0.1", + "eslint": "^8.53.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.5.0", - "eslint-config-standard-with-typescript": "^22.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.0.0", + "eslint-config-standard-with-typescript": "^39.1.1", "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-n": "^15.0.0", - "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "husky": "^8.0.1", - "lint-staged": "^13.0.3", + "husky": "^9.1.7", + "js-yaml": "^4.1.0", + "lint-staged": "^15.5.0", "postcss": "^8.0.0", - "prettier": "^2.7.1", + "prettier": "^3.1.0", "purgecss-webpack-plugin": "^4.1.3", "react-app-rewired": "^2.2.1", "react-scripts": "5.0.1", - "sass": "^1.54.4", - "tsconfig-paths-webpack-plugin": "^4.0.0", - "typescript": "*", - "web-vitals": "^2.1.4" + "sass": "1.54.4", + "source-map-explorer": "^2.5.3", + "typescript": "^4.9.5", + "yaml-loader": "^0.8.0" }, - "packageManager": "pnpm@7.9.5", + "packageManager": "pnpm@9.7.0", "engines": { - "node": ">=16.17", - "pnpm": ">=7" + "node": ">=20", + "pnpm": ">=9" }, "license": "MIT" } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 22d75f0e8..842009a70 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -1,1831 +1,1268 @@ -lockfileVersion: 5.4 - -specifiers: - '@babel/core': ^7.18.10 - '@babel/plugin-syntax-flow': ^7.18.6 - '@babel/plugin-transform-react-jsx': ^7.14.9 - '@commitlint/cli': ^17.0.3 - '@commitlint/config-conventional': ^17.0.3 - '@fullhuman/postcss-purgecss': ^4.1.3 - '@popperjs/core': ^2.11.5 - '@testing-library/dom': ^8.17.1 - '@testing-library/jest-dom': ^4.2.4 - '@testing-library/react': ^13.3.0 - '@testing-library/user-event': ^13.5.0 - '@types/jest': ^27.5.2 - '@types/lodash': ^4.14.184 - '@types/marked': ^4.0.6 - '@types/node': ^16.11.47 - '@types/qs': ^6.9.7 - '@types/react': ^18.0.17 - '@types/react-dom': ^18.0.6 - '@types/react-helmet': ^6.1.5 - '@typescript-eslint/eslint-plugin': ^5.0.0 - '@typescript-eslint/parser': ^5.33.0 - ahooks: ^3.7.0 - axios: ^0.27.2 - bootstrap: ^5.2.0 - bootstrap-icons: ^1.9.1 - classnames: ^2.3.1 - codemirror: 5.65.0 - commitizen: ^4.2.5 - conventional-changelog-cli: ^2.2.2 - copy-to-clipboard: ^3.3.2 - customize-cra: ^1.0.0 - cz-conventional-changelog: ^3.3.0 - dayjs: ^1.11.5 - env-cmd: ^10.1.0 - eslint: ^8.0.1 - eslint-config-airbnb: ^19.0.4 - eslint-config-airbnb-typescript: ^17.0.0 - eslint-config-prettier: ^8.5.0 - eslint-config-standard-with-typescript: ^22.0.0 - eslint-plugin-import: ^2.25.2 - eslint-plugin-jsx-a11y: ^6.6.1 - eslint-plugin-n: ^15.0.0 - eslint-plugin-prettier: ^4.2.1 - eslint-plugin-promise: ^6.0.0 - eslint-plugin-react: ^7.30.1 - eslint-plugin-react-hooks: ^4.6.0 - highlight.js: ^11.6.0 - husky: ^8.0.1 - i18next: ^21.9.0 - i18next-chained-backend: ^3.0.2 - i18next-http-backend: ^1.4.1 - i18next-localstorage-backend: ^3.1.3 - katex: ^0.16.2 - lint-staged: ^13.0.3 - lodash: ^4.17.21 - marked: ^4.0.19 - mermaid: ^9.1.7 - next-share: ^0.18.1 - postcss: ^8.0.0 - prettier: ^2.7.1 - purgecss-webpack-plugin: ^4.1.3 - qs: ^6.11.0 - react: ^18.2.0 - react-app-rewired: ^2.2.1 - react-bootstrap: ^2.5.0 - react-dom: ^18.2.0 - react-helmet-async: ^1.3.0 - react-i18next: ^11.18.3 - react-router-dom: ^6.4.0 - react-scripts: 5.0.1 - sass: ^1.54.4 - swr: ^1.3.0 - tsconfig-paths-webpack-plugin: ^4.0.0 - typescript: '*' - web-vitals: ^2.1.4 - zustand: ^4.1.1 - -dependencies: - '@testing-library/jest-dom': 4.2.4 - ahooks: 3.7.1_react@18.2.0 - axios: 0.27.2 - bootstrap: 5.2.1_@popperjs+core@2.11.6 - bootstrap-icons: 1.9.1 - classnames: 2.3.2 - codemirror: 5.65.0 - copy-to-clipboard: 3.3.2 - dayjs: 1.11.5 - highlight.js: 11.6.0 - i18next: 21.9.2 - i18next-chained-backend: 3.1.0 - i18next-http-backend: 1.4.4 - i18next-localstorage-backend: 3.1.3 - katex: 0.16.2 - lodash: 4.17.21 - marked: 4.1.0 - mermaid: 9.1.7 - next-share: 0.18.1_lbqamd2wfmenkveygahn4wdfcq - qs: 6.11.0 - react: 18.2.0 - react-bootstrap: 2.5.0_7ey2zzynotv32rpkwno45fsx4e - react-dom: 18.2.0_react@18.2.0 - react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y - react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq - react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y - swr: 1.3.0_react@18.2.0 - zustand: 4.1.1_react@18.2.0 - -devDependencies: - '@babel/core': 7.19.1 - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.1 - '@commitlint/cli': 17.1.2 - '@commitlint/config-conventional': 17.1.0 - '@fullhuman/postcss-purgecss': 4.1.3_postcss@8.4.16 - '@popperjs/core': 2.11.6 - '@testing-library/dom': 8.18.1 - '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y - '@testing-library/user-event': 13.5.0_znccgeejomvff3jrsk3ljovfpu - '@types/jest': 27.5.2 - '@types/lodash': 4.14.185 - '@types/marked': 4.0.7 - '@types/node': 16.11.59 - '@types/qs': 6.9.7 - '@types/react': 18.0.20 - '@types/react-dom': 18.0.6 - '@types/react-helmet': 6.1.5 - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - commitizen: 4.2.5 - conventional-changelog-cli: 2.2.2 - customize-cra: 1.0.0 - cz-conventional-changelog: 3.3.0 - env-cmd: 10.1.0 - eslint: 8.23.1 - eslint-config-airbnb: 19.0.4_4zstfqq5uopk5xuvotejlnl36y - eslint-config-airbnb-typescript: 17.0.0_j57hrpt2hfp47otngkwtnuyxpa - eslint-config-prettier: 8.5.0_eslint@8.23.1 - eslint-config-standard-with-typescript: 22.0.0_fsqc7gnfr7ufpr4slszrtm5abq - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-jsx-a11y: 6.6.1_eslint@8.23.1 - eslint-plugin-n: 15.2.5_eslint@8.23.1 - eslint-plugin-prettier: 4.2.1_cabrci5exjdaojcvd6xoxgeowu - eslint-plugin-promise: 6.0.1_eslint@8.23.1 - eslint-plugin-react: 7.31.8_eslint@8.23.1 - eslint-plugin-react-hooks: 4.6.0_eslint@8.23.1 - husky: 8.0.1 - lint-staged: 13.0.3 - postcss: 8.4.16 - prettier: 2.7.1 - purgecss-webpack-plugin: 4.1.3 - react-app-rewired: 2.2.1_react-scripts@5.0.1 - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i - sass: 1.54.9 - tsconfig-paths-webpack-plugin: 4.0.0 - typescript: 4.8.3 - web-vitals: 2.1.4 +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@codemirror/lang-markdown': + specifier: ^6.2.4 + version: 6.3.1 + '@codemirror/language-data': + specifier: ^6.5.0 + version: 6.5.1(@codemirror/view@6.35.3) + '@codemirror/state': + specifier: ^6.5.0 + version: 6.5.0 + '@codemirror/view': + specifier: ^6.26.1 + version: 6.35.3 + axios: + specifier: ^1.7.7 + version: 1.7.9 + bootstrap: + specifier: ^5.3.2 + version: 5.3.3(@popperjs/core@2.11.8) + bootstrap-icons: + specifier: ^1.10.5 + version: 1.11.3 + classnames: + specifier: ^2.3.1 + version: 2.5.1 + codemirror: + specifier: ^6.0.1 + version: 6.0.1(@lezer/common@1.2.3) + color: + specifier: ^4.2.3 + version: 4.2.3 + copy-to-clipboard: + specifier: ^3.3.2 + version: 3.3.3 + dayjs: + specifier: ^1.11.5 + version: 1.11.13 + diff: + specifier: ^5.1.0 + version: 5.2.0 + front-matter: + specifier: ^4.0.2 + version: 4.0.2 + i18next: + specifier: ^21.9.0 + version: 21.10.0 + js-sha256: + specifier: 0.11.0 + version: 0.11.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + marked: + specifier: ^4.0.19 + version: 4.3.0 + next-share: + specifier: ^0.18.1 + version: 0.18.4(react@18.3.1) + qrcode: + specifier: ^1.5.1 + version: 1.5.4 + qs: + specifier: ^6.11.0 + version: 6.13.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-bootstrap: + specifier: ^2.10.0 + version: 2.10.6(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-helmet-async: + specifier: ^1.3.0 + version: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-i18next: + specifier: ^11.18.3 + version: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.0.2 + version: 7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + semver: + specifier: ^7.3.8 + version: 7.6.3 + swr: + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) + zustand: + specifier: ^5.0.2 + version: 5.0.2(@types/react@18.3.16)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) + devDependencies: + '@commitlint/cli': + specifier: ^17.0.3 + version: 17.8.1 + '@commitlint/config-conventional': + specifier: ^17.2.0 + version: 17.8.1 + '@fullhuman/postcss-purgecss': + specifier: ^4.1.3 + version: 4.1.3(postcss@8.4.49) + '@testing-library/dom': + specifier: ^8.17.1 + version: 8.20.1 + '@testing-library/jest-dom': + specifier: ^4.2.4 + version: 4.2.4 + '@testing-library/react': + specifier: ^13.3.0 + version: 13.4.0(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^13.5.0 + version: 13.5.0(@testing-library/dom@8.20.1) + '@types/color': + specifier: ^3.0.3 + version: 3.0.6 + '@types/dompurify': + specifier: ^2.4.0 + version: 2.4.0 + '@types/jest': + specifier: ^27.5.2 + version: 27.5.2 + '@types/lodash': + specifier: ^4.14.184 + version: 4.17.13 + '@types/marked': + specifier: ^4.0.6 + version: 4.3.2 + '@types/node': + specifier: ^16.11.47 + version: 16.18.121 + '@types/qs': + specifier: ^6.9.7 + version: 6.9.17 + '@types/react': + specifier: ^18.0.17 + version: 18.3.16 + '@types/react-dom': + specifier: ^18.0.6 + version: 18.3.5(@types/react@18.3.16) + '@typescript-eslint/eslint-plugin': + specifier: ^6.11.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^6.11.0 + version: 6.21.0(eslint@8.57.1)(typescript@4.9.5) + buffer: + specifier: 6.0.3 + version: 6.0.3 + customize-cra: + specifier: ^1.0.0 + version: 1.0.0 + eslint: + specifier: ^8.53.0 + version: 8.57.1 + eslint-config-airbnb: + specifier: ^19.0.4 + version: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.2(eslint@8.57.1))(eslint@8.57.1) + eslint-config-airbnb-typescript: + specifier: ^17.1.0 + version: 17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.1.0(eslint@8.57.1) + eslint-config-standard-with-typescript: + specifier: ^39.1.1 + version: 39.1.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@4.9.5) + eslint-plugin-import: + specifier: ^2.25.2 + version: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + eslint-plugin-jsx-a11y: + specifier: ^6.8.0 + version: 6.10.2(eslint@8.57.1) + eslint-plugin-n: + specifier: '^15.0.0 || ^16.0.0 ' + version: 16.6.2(eslint@8.57.1) + eslint-plugin-prettier: + specifier: ^5.0.0 + version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.2) + eslint-plugin-promise: + specifier: ^6.0.0 + version: 6.6.0(eslint@8.57.1) + eslint-plugin-react: + specifier: ^7.33.2 + version: 7.37.2(eslint@8.57.1) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.2(eslint@8.57.1) + husky: + specifier: ^9.1.7 + version: 9.1.7 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + lint-staged: + specifier: ^15.5.0 + version: 15.5.0 + postcss: + specifier: ^8.0.0 + version: 8.4.49 + prettier: + specifier: ^3.1.0 + version: 3.4.2 + purgecss-webpack-plugin: + specifier: ^4.1.3 + version: 4.1.3(webpack@5.97.1) + react-app-rewired: + specifier: ^2.2.1 + version: 2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@types/babel__core@7.20.5)(eslint@8.57.1)(react@18.3.1)(sass@1.54.4)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))(type-fest@1.4.0)(typescript@4.9.5)) + react-scripts: + specifier: 5.0.1 + version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@types/babel__core@7.20.5)(eslint@8.57.1)(react@18.3.1)(sass@1.54.4)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))(type-fest@1.4.0)(typescript@4.9.5) + sass: + specifier: 1.54.4 + version: 1.54.4 + source-map-explorer: + specifier: ^2.5.3 + version: 2.5.3 + typescript: + specifier: ^4.9.5 + version: 4.9.5 + yaml-loader: + specifier: ^0.8.0 + version: 0.8.1 packages: - /@ampproject/remapping/2.2.0: - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.15 - /@apideck/better-ajv-errors/0.3.6_ajv@8.11.0: + '@apideck/better-ajv-errors@0.3.6': resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} engines: {node: '>=10'} peerDependencies: ajv: '>=8' - dependencies: - ajv: 8.11.0 - json-schema: 0.4.0 - jsonpointer: 5.0.1 - leven: 3.1.0 - /@babel/code-frame/7.18.6: - resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.18.6 - /@babel/compat-data/7.19.1: - resolution: {integrity: sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg==} + '@babel/compat-data@7.26.3': + resolution: {integrity: sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==} engines: {node: '>=6.9.0'} - /@babel/core/7.19.1: - resolution: {integrity: sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw==} + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.0 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helpers': 7.19.0 - '@babel/parser': 7.19.1 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 - convert-source-map: 1.8.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - /@babel/eslint-parser/7.19.1_zdglor7vg7osicr5spasq6cc5a: - resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==} + '@babel/eslint-parser@7.25.9': + resolution: {integrity: sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: - '@babel/core': '>=7.11.0' - eslint: ^7.5.0 || ^8.0.0 - dependencies: - '@babel/core': 7.19.1 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.23.1 - eslint-visitor-keys: 2.1.0 - semver: 6.3.0 - - /@babel/generator/7.19.0: - resolution: {integrity: sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - '@jridgewell/gen-mapping': 0.3.2 - jsesc: 2.5.2 + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - /@babel/helper-annotate-as-pure/7.18.6: - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + '@babel/generator@7.26.3': + resolution: {integrity: sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - /@babel/helper-builder-binary-assignment-operator-visitor/7.18.9: - resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.19.0 - /@babel/helper-compilation-targets/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg==} + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.19.1 - '@babel/core': 7.19.1 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.21.4 - semver: 6.3.0 - /@babel/helper-create-class-features-plugin/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==} + '@babel/helper-create-class-features-plugin@7.25.9': + resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - transitivePeerDependencies: - - supports-color - /@babel/helper-create-regexp-features-plugin/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} + '@babel/helper-create-regexp-features-plugin@7.26.3': + resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.2.1 - /@babel/helper-define-polyfill-provider/0.3.3_@babel+core@7.19.1: - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + '@babel/helper-define-polyfill-provider@0.6.3': + resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==} peerDependencies: - '@babel/core': ^7.4.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - - /@babel/helper-environment-visitor/7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - - /@babel/helper-explode-assignable-expression/7.18.6: - resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - - /@babel/helper-function-name/7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.19.0 - - /@babel/helper-hoist-variables/7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - - /@babel/helper-member-expression-to-functions/7.18.9: - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - - /@babel/helper-module-imports/7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - - /@babel/helper-module-transforms/7.19.0: - resolution: {integrity: sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /@babel/helper-optimise-call-expression/7.18.6: - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + '@babel/helper-member-expression-to-functions@7.25.9': + resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - /@babel/helper-plugin-utils/7.19.0: - resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} - /@babel/helper-remap-async-to-generator/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-wrap-function': 7.19.0 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/helper-replace-supers/7.19.1: - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} + '@babel/helper-optimise-call-expression@7.25.9': + resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/helper-simple-access/7.18.6: - resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==} + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 - /@babel/helper-skip-transparent-expression-wrappers/7.18.9: - resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==} + '@babel/helper-remap-async-to-generator@7.25.9': + resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 + peerDependencies: + '@babel/core': ^7.0.0 - /@babel/helper-split-export-declaration/7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + '@babel/helper-replace-supers@7.25.9': + resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.19.0 + peerDependencies: + '@babel/core': ^7.0.0 - /@babel/helper-string-parser/7.18.10: - resolution: {integrity: sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==} + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-identifier/7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option/7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - /@babel/helper-wrap-function/7.19.0: - resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==} + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-function-name': 7.19.0 - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/helpers/7.19.0: - resolution: {integrity: sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==} + '@babel/helper-wrap-function@7.25.9': + resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.18.10 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/highlight/7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - /@babel/parser/7.19.1: - resolution: {integrity: sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A==} + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} engines: {node: '>=6.0.0'} hasBin: true - dependencies: - '@babel/types': 7.19.0 - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9': + resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - - /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.19.1 - - /@babel/plugin-proposal-async-generator-functions/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color - - /@babel/plugin-proposal-class-properties/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/plugin-proposal-class-static-block/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9': + resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.12.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.0.0 - /@babel/plugin-proposal-decorators/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-LfIKNBBY7Q1OX5C4xAgRQffOg2OnhAo9fnbcOHgOC9Yytm2Sw+4XqHufRYU86tHomzepxtvuVaNO+3EVKR4ivw==} + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9': + resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.0.0 - /@babel/plugin-proposal-dynamic-import/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9': + resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.19.1 + '@babel/core': ^7.13.0 - /@babel/plugin-proposal-export-namespace-from/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9': + resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.19.1 + '@babel/core': ^7.0.0 - /@babel/plugin-proposal-json-strings/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.19.1 - /@babel/plugin-proposal-logical-assignment-operators/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} + '@babel/plugin-proposal-decorators@7.25.9': + resolution: {integrity: sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.19.1 - /@babel/plugin-proposal-nullish-coalescing-operator/7.18.6_@babel+core@7.19.1: + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.19.1 - /@babel/plugin-proposal-numeric-separator/7.18.6_@babel+core@7.19.1: + '@babel/plugin-proposal-numeric-separator@7.18.6': resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.19.1 - - /@babel/plugin-proposal-object-rest-spread/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.1 - '@babel/core': 7.19.1 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.19.1 - - /@babel/plugin-proposal-optional-catch-binding/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.19.1 - /@babel/plugin-proposal-optional-chaining/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} + '@babel/plugin-proposal-optional-chaining@7.21.0': + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.19.1 - /@babel/plugin-proposal-private-methods/7.18.6_@babel+core@7.19.1: + '@babel/plugin-proposal-private-methods@7.18.6': resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/plugin-proposal-private-property-in-object/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color - /@babel/plugin-proposal-unicode-property-regex/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} + '@babel/plugin-proposal-private-property-in-object@7.21.11': + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-async-generators/7.8.4_@babel+core@7.19.1: + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-bigint/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-bigint@7.8.3': resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-class-properties/7.12.13_@babel+core@7.19.1: + '@babel/plugin-syntax-class-properties@7.12.13': resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-class-static-block/7.14.5_@babel+core@7.19.1: + '@babel/plugin-syntax-class-static-block@7.14.5': resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-decorators/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==} + '@babel/plugin-syntax-decorators@7.25.9': + resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - - /@babel/plugin-syntax-dynamic-import/7.8.3_@babel+core@7.19.1: - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-export-namespace-from/7.8.3_@babel+core@7.19.1: - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + '@babel/plugin-syntax-flow@7.26.0': + resolution: {integrity: sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-flow/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-import-assertions/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==} + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-import-meta/7.10.4_@babel+core@7.19.1: + '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-json-strings/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-json-strings@7.8.3': resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-jsx/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-logical-assignment-operators/7.10.4_@babel+core@7.19.1: + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.19.1: + '@babel/plugin-syntax-numeric-separator@7.10.4': resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-object-rest-spread/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-object-rest-spread@7.8.3': resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-optional-catch-binding/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-optional-catch-binding@7.8.3': resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.19.1: + '@babel/plugin-syntax-optional-chaining@7.8.3': resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.19.1: + '@babel/plugin-syntax-private-property-in-object@7.14.5': resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-top-level-await/7.14.5_@babel+core@7.19.1: + '@babel/plugin-syntax-top-level-await@7.14.5': resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-syntax-typescript/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==} + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-arrow-functions/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': ^7.0.0 - /@babel/plugin-transform-async-to-generator/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} + '@babel/plugin-transform-arrow-functions@7.25.9': + resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-remap-async-to-generator': 7.18.9_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-block-scoped-functions/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + '@babel/plugin-transform-async-generator-functions@7.25.9': + resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-block-scoping/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==} + '@babel/plugin-transform-async-to-generator@7.25.9': + resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-classes/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} + '@babel/plugin-transform-block-scoped-functions@7.25.9': + resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.18.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-computed-properties/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} + '@babel/plugin-transform-block-scoping@7.25.9': + resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-destructuring/7.18.13_@babel+core@7.19.1: - resolution: {integrity: sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow==} + '@babel/plugin-transform-class-properties@7.25.9': + resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-dotall-regex/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + '@babel/plugin-transform-class-static-block@7.26.0': + resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': ^7.12.0 - /@babel/plugin-transform-duplicate-keys/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + '@babel/plugin-transform-classes@7.25.9': + resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-exponentiation-operator/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + '@babel/plugin-transform-computed-properties@7.25.9': + resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-flow-strip-types/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-sgeMlNaQVbCSpgLSKP4ZZKfsJVnFnNQlUSk6gPYzR/q7tzCgQF2t8RBKAP6cKJeZdveei7Q7Jm527xepI8lNLg==} + '@babel/plugin-transform-destructuring@7.25.9': + resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.19.1 - /@babel/plugin-transform-for-of/7.18.8_@babel+core@7.19.1: - resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} + '@babel/plugin-transform-dotall-regex@7.25.9': + resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-function-name/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + '@babel/plugin-transform-duplicate-keys@7.25.9': + resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-literals/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': ^7.0.0 - /@babel/plugin-transform-member-expression-literals/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + '@babel/plugin-transform-dynamic-import@7.25.9': + resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-modules-amd/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==} + '@babel/plugin-transform-exponentiation-operator@7.26.3': + resolution: {integrity: sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-modules-commonjs/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==} + '@babel/plugin-transform-export-namespace-from@7.25.9': + resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-simple-access': 7.18.6 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-modules-systemjs/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A==} + '@babel/plugin-transform-flow-strip-types@7.25.9': + resolution: {integrity: sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-identifier': 7.19.1 - babel-plugin-dynamic-import-node: 2.3.3 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-modules-umd/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + '@babel/plugin-transform-for-of@7.25.9': + resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-transforms': 7.19.0 - '@babel/helper-plugin-utils': 7.19.0 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-named-capturing-groups-regex/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} + '@babel/plugin-transform-function-name@7.25.9': + resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/core': ^7.0.0-0 - /@babel/plugin-transform-new-target/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + '@babel/plugin-transform-json-strings@7.25.9': + resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-object-super/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + '@babel/plugin-transform-literals@7.25.9': + resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-parameters/7.18.8_@babel+core@7.19.1: - resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} + '@babel/plugin-transform-logical-assignment-operators@7.25.9': + resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-property-literals/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + '@babel/plugin-transform-member-expression-literals@7.25.9': + resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-react-constant-elements/7.18.12_@babel+core@7.19.1: - resolution: {integrity: sha512-Q99U9/ttiu+LMnRU8psd23HhvwXmKWDQIpocm0JKaICcZHnw+mdQbHm6xnSy7dOl8I5PELakYtNBubNQlBXbZw==} + '@babel/plugin-transform-modules-amd@7.25.9': + resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-react-display-name/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} + '@babel/plugin-transform-modules-commonjs@7.26.3': + resolution: {integrity: sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-react-jsx-development/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} + '@babel/plugin-transform-modules-systemjs@7.25.9': + resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.1 - /@babel/plugin-transform-react-jsx/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} + '@babel/plugin-transform-modules-umd@7.25.9': + resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.19.1 - '@babel/types': 7.19.0 - /@babel/plugin-transform-react-pure-annotations/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9': + resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.25.9': + resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-regenerator/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9': + resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - regenerator-transform: 0.15.0 - /@babel/plugin-transform-reserved-words/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + '@babel/plugin-transform-numeric-separator@7.25.9': + resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-runtime/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-2nJjTUFIzBMP/f/miLxEK9vxwW/KUXsdvN4sR//TmuDhe6yU2h57WmIOE12Gng3MDP/xpjUV/ToZRdcf8Yj4fA==} + '@babel/plugin-transform-object-rest-spread@7.25.9': + resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.19.0 - babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.19.1 - babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.19.1 - babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.19.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-shorthand-properties/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + '@babel/plugin-transform-object-super@7.25.9': + resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-spread/7.19.0_@babel+core@7.19.1: - resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} + '@babel/plugin-transform-optional-catch-binding@7.25.9': + resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - /@babel/plugin-transform-sticky-regex/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + '@babel/plugin-transform-optional-chaining@7.25.9': + resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-template-literals/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + '@babel/plugin-transform-parameters@7.25.9': + resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-typeof-symbol/7.18.9_@babel+core@7.19.1: - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + '@babel/plugin-transform-private-methods@7.25.9': + resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-typescript/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-+ILcOU+6mWLlvCwnL920m2Ow3wWx3Wo8n2t5aROQmV55GZt+hOiLvBaa3DNzRjSEHa1aauRs4/YLmkCfFkhhRQ==} + '@babel/plugin-transform-private-property-in-object@7.25.9': + resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-syntax-typescript': 7.18.6_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color - /@babel/plugin-transform-unicode-escapes/7.18.10_@babel+core@7.19.1: - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + '@babel/plugin-transform-property-literals@7.25.9': + resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/plugin-transform-unicode-regex/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + '@babel/plugin-transform-react-constant-elements@7.25.9': + resolution: {integrity: sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-create-regexp-features-plugin': 7.19.0_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - /@babel/preset-env/7.19.1_@babel+core@7.19.1: - resolution: {integrity: sha512-c8B2c6D16Lp+Nt6HcD+nHl0VbPKVnNPTpszahuxJJnurfMtKeZ80A+qUv48Y7wqvS+dTFuLuaM9oYxyNHbCLWA==} + '@babel/plugin-transform-react-display-name@7.25.9': + resolution: {integrity: sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.1 - '@babel/core': 7.19.1 - '@babel/helper-compilation-targets': 7.19.1_@babel+core@7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-async-generator-functions': 7.19.1_@babel+core@7.19.1 - '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-class-static-block': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-dynamic-import': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-export-namespace-from': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-json-strings': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-logical-assignment-operators': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-object-rest-spread': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-optional-catch-binding': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.19.1 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.19.1 - '@babel/plugin-syntax-class-static-block': 7.14.5_@babel+core@7.19.1 - '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-import-assertions': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.19.1 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.19.1 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-private-property-in-object': 7.14.5_@babel+core@7.19.1 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.19.1 - '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-async-to-generator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-block-scoped-functions': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-block-scoping': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-classes': 7.19.0_@babel+core@7.19.1 - '@babel/plugin-transform-computed-properties': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-destructuring': 7.18.13_@babel+core@7.19.1 - '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-duplicate-keys': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-exponentiation-operator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-for-of': 7.18.8_@babel+core@7.19.1 - '@babel/plugin-transform-function-name': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-literals': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-member-expression-literals': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-modules-amd': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-modules-commonjs': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-modules-systemjs': 7.19.0_@babel+core@7.19.1 - '@babel/plugin-transform-modules-umd': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1_@babel+core@7.19.1 - '@babel/plugin-transform-new-target': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-object-super': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-parameters': 7.18.8_@babel+core@7.19.1 - '@babel/plugin-transform-property-literals': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-regenerator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-reserved-words': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-shorthand-properties': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-spread': 7.19.0_@babel+core@7.19.1 - '@babel/plugin-transform-sticky-regex': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-template-literals': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-typeof-symbol': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-transform-unicode-escapes': 7.18.10_@babel+core@7.19.1 - '@babel/plugin-transform-unicode-regex': 7.18.6_@babel+core@7.19.1 - '@babel/preset-modules': 0.1.5_@babel+core@7.19.1 - '@babel/types': 7.19.0 - babel-plugin-polyfill-corejs2: 0.3.3_@babel+core@7.19.1 - babel-plugin-polyfill-corejs3: 0.6.0_@babel+core@7.19.1 - babel-plugin-polyfill-regenerator: 0.4.1_@babel+core@7.19.1 - core-js-compat: 3.25.2 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - /@babel/preset-modules/0.1.5_@babel+core@7.19.1: - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + '@babel/plugin-transform-react-jsx-development@7.25.9': + resolution: {integrity: sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.19.1 - '@babel/types': 7.19.0 - esutils: 2.0.3 - /@babel/preset-react/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.1 - '@babel/plugin-transform-react-jsx-development': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-react-pure-annotations': 7.18.6_@babel+core@7.19.1 - /@babel/preset-typescript/7.18.6_@babel+core@7.19.1: - resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} + '@babel/plugin-transform-react-pure-annotations@7.25.9': + resolution: {integrity: sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-plugin-utils': 7.19.0 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.19.1_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color - /@babel/runtime-corejs3/7.19.1: - resolution: {integrity: sha512-j2vJGnkopRzH+ykJ8h68wrHnEUmtK//E723jjixiAl/PPf6FhqY/vYRcMVlNydRKQjQsTsYEjpx+DZMIvnGk/g==} + '@babel/plugin-transform-regenerator@7.25.9': + resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==} engines: {node: '>=6.9.0'} - dependencies: - core-js-pure: 3.25.2 - regenerator-runtime: 0.13.9 + peerDependencies: + '@babel/core': ^7.0.0-0 - /@babel/runtime/7.19.0: - resolution: {integrity: sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==} + '@babel/plugin-transform-regexp-modifiers@7.26.0': + resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==} engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.9 + peerDependencies: + '@babel/core': ^7.0.0 - /@babel/template/7.18.10: - resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} + '@babel/plugin-transform-reserved-words@7.25.9': + resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/parser': 7.19.1 - '@babel/types': 7.19.0 + peerDependencies: + '@babel/core': ^7.0.0-0 - /@babel/traverse/7.19.1: - resolution: {integrity: sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA==} + '@babel/plugin-transform-runtime@7.25.9': + resolution: {integrity: sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.19.0 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.19.1 - '@babel/types': 7.19.0 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + peerDependencies: + '@babel/core': ^7.0.0-0 - /@babel/types/7.19.0: - resolution: {integrity: sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==} + '@babel/plugin-transform-shorthand-properties@7.25.9': + resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.18.10 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.25.9': + resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.25.9': + resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.25.9': + resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.25.9': + resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.26.3': + resolution: {integrity: sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.25.9': + resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.25.9': + resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.25.9': + resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9': + resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.26.0': + resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.26.3': + resolution: {integrity: sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 - /@bcoe/v8-coverage/0.2.3: + '@babel/preset-typescript@7.26.0': + resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.4': + resolution: {integrity: sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - /@braintree/sanitize-url/6.0.0: - resolution: {integrity: sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==} - dev: false + '@codemirror/autocomplete@6.18.3': + resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 + + '@codemirror/commands@6.7.1': + resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==} + + '@codemirror/lang-angular@0.1.3': + resolution: {integrity: sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==} + + '@codemirror/lang-cpp@6.0.2': + resolution: {integrity: sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.9': + resolution: {integrity: sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==} + + '@codemirror/lang-java@6.0.1': + resolution: {integrity: sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==} + + '@codemirror/lang-javascript@6.2.2': + resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==} + + '@codemirror/lang-json@6.0.1': + resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.2.2': + resolution: {integrity: sha512-7Dm841fk37+JQW6j2rI1/uGkJyESrjzyhiIkaLjbbR0U6aFFQvMrJn35WxQreRMADMhzkyVkZM4467OR7GR8nQ==} + + '@codemirror/lang-markdown@6.3.1': + resolution: {integrity: sha512-y3sSPuQjBKZQbQwe3ZJKrSW6Silyl9PnrU/Mf0m2OQgIlPoSYTtOvEL7xs94SVMkb8f4x+SQFnzXPdX4Wk2lsg==} + + '@codemirror/lang-php@6.0.1': + resolution: {integrity: sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==} + + '@codemirror/lang-python@6.1.6': + resolution: {integrity: sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==} + + '@codemirror/lang-rust@6.0.1': + resolution: {integrity: sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.8.0': + resolution: {integrity: sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} - /@commitlint/cli/17.1.2: - resolution: {integrity: sha512-h/4Hlka3bvCLbnxf0Er2ri5A44VMlbMSkdTRp8Adv2tRiklSTRIoPGs7OEXDv3EoDs2AAzILiPookgM4Gi7LOw==} + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.2': + resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==} + + '@codemirror/language-data@6.5.1': + resolution: {integrity: sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==} + + '@codemirror/language@6.10.6': + resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==} + + '@codemirror/legacy-modes@6.4.2': + resolution: {integrity: sha512-HsvWu08gOIIk303eZQCal4H4t65O/qp1V4ul4zVa3MHK5FJ0gz3qz3O55FIkm+aQUcshUOjBx38t2hPiJwW5/g==} + + '@codemirror/lint@6.8.4': + resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==} + + '@codemirror/search@6.5.8': + resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==} + + '@codemirror/state@6.5.0': + resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==} + + '@codemirror/view@6.35.3': + resolution: {integrity: sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==} + + '@commitlint/cli@17.8.1': + resolution: {integrity: sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg==} engines: {node: '>=v14'} hasBin: true - dependencies: - '@commitlint/format': 17.0.0 - '@commitlint/lint': 17.1.0 - '@commitlint/load': 17.1.2 - '@commitlint/read': 17.1.0 - '@commitlint/types': 17.0.0 - execa: 5.1.1 - lodash: 4.17.21 - resolve-from: 5.0.0 - resolve-global: 1.0.0 - yargs: 17.5.1 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - dev: true - /@commitlint/config-conventional/17.1.0: - resolution: {integrity: sha512-WU2p0c9/jLi8k2q2YrDV96Y8XVswQOceIQ/wyJvQxawJSCasLdRB3kUIYdNjOCJsxkpoUlV/b90ZPxp1MYZDiA==} + '@commitlint/config-conventional@17.8.1': + resolution: {integrity: sha512-NxCOHx1kgneig3VLauWJcDWS40DVjg7nKOpBEEK9E5fjJpQqLCilcnKkIIjdBH98kEO1q3NpE5NSrZ2kl/QGJg==} engines: {node: '>=v14'} - dependencies: - conventional-changelog-conventionalcommits: 5.0.0 - dev: true - /@commitlint/config-validator/17.1.0: - resolution: {integrity: sha512-Q1rRRSU09ngrTgeTXHq6ePJs2KrI+axPTgkNYDWSJIuS1Op4w3J30vUfSXjwn5YEJHklK3fSqWNHmBhmTR7Vdg==} + '@commitlint/config-validator@17.8.1': + resolution: {integrity: sha512-UUgUC+sNiiMwkyiuIFR7JG2cfd9t/7MV8VB4TZ+q02ZFkHoduUS4tJGsCBWvBOGD9Btev6IecPMvlWUfJorkEA==} engines: {node: '>=v14'} - dependencies: - '@commitlint/types': 17.0.0 - ajv: 8.11.0 - dev: true - /@commitlint/ensure/17.0.0: - resolution: {integrity: sha512-M2hkJnNXvEni59S0QPOnqCKIK52G1XyXBGw51mvh7OXDudCmZ9tZiIPpU882p475Mhx48Ien1MbWjCP1zlyC0A==} + '@commitlint/ensure@17.8.1': + resolution: {integrity: sha512-xjafwKxid8s1K23NFpL8JNo6JnY/ysetKo8kegVM7c8vs+kWLP8VrQq+NbhgVlmCojhEDbzQKp4eRXSjVOGsow==} engines: {node: '>=v14'} - dependencies: - '@commitlint/types': 17.0.0 - lodash: 4.17.21 - dev: true - /@commitlint/execute-rule/17.0.0: - resolution: {integrity: sha512-nVjL/w/zuqjCqSJm8UfpNaw66V9WzuJtQvEnCrK4jDw6qKTmZB+1JQ8m6BQVZbNBcwfYdDNKnhIhqI0Rk7lgpQ==} + '@commitlint/execute-rule@17.8.1': + resolution: {integrity: sha512-JHVupQeSdNI6xzA9SqMF+p/JjrHTcrJdI02PwesQIDCIGUrv04hicJgCcws5nzaoZbROapPs0s6zeVHoxpMwFQ==} engines: {node: '>=v14'} - dev: true - /@commitlint/format/17.0.0: - resolution: {integrity: sha512-MZzJv7rBp/r6ZQJDEodoZvdRM0vXu1PfQvMTNWFb8jFraxnISMTnPBWMMjr2G/puoMashwaNM//fl7j8gGV5lA==} + '@commitlint/format@17.8.1': + resolution: {integrity: sha512-f3oMTyZ84M9ht7fb93wbCKmWxO5/kKSbwuYvS867duVomoOsgrgljkGGIztmT/srZnaiGbaK8+Wf8Ik2tSr5eg==} engines: {node: '>=v14'} - dependencies: - '@commitlint/types': 17.0.0 - chalk: 4.1.2 - dev: true - /@commitlint/is-ignored/17.1.0: - resolution: {integrity: sha512-JITWKDMHhIh8IpdIbcbuH9rEQJty1ZWelgjleTFrVRAcEwN/sPzk1aVUXRIZNXMJWbZj8vtXRJnFihrml8uECQ==} + '@commitlint/is-ignored@17.8.1': + resolution: {integrity: sha512-UshMi4Ltb4ZlNn4F7WtSEugFDZmctzFpmbqvpyxD3la510J+PLcnyhf9chs7EryaRFJMdAKwsEKfNK0jL/QM4g==} engines: {node: '>=v14'} - dependencies: - '@commitlint/types': 17.0.0 - semver: 7.3.7 - dev: true - /@commitlint/lint/17.1.0: - resolution: {integrity: sha512-ltpqM2ogt/+SDhUaScFo0MdscncEF96lvQTPMM/VTTWlw7sTGLLWkOOppsee2MN/uLNNWjQ7kqkd4h6JqoM9AQ==} + '@commitlint/lint@17.8.1': + resolution: {integrity: sha512-aQUlwIR1/VMv2D4GXSk7PfL5hIaFSfy6hSHV94O8Y27T5q+DlDEgd/cZ4KmVI+MWKzFfCTiTuWqjfRSfdRllCA==} engines: {node: '>=v14'} - dependencies: - '@commitlint/is-ignored': 17.1.0 - '@commitlint/parse': 17.0.0 - '@commitlint/rules': 17.0.0 - '@commitlint/types': 17.0.0 - dev: true - /@commitlint/load/17.1.2: - resolution: {integrity: sha512-sk2p/jFYAWLChIfOIp/MGSIn/WzZ0vkc3afw+l4X8hGEYkvDe4gQUUAVxjl/6xMRn0HgnSLMZ04xXh5pkTsmgg==} + '@commitlint/load@17.8.1': + resolution: {integrity: sha512-iF4CL7KDFstP1kpVUkT8K2Wl17h2yx9VaR1ztTc8vzByWWcbO/WaKwxsnCOqow9tVAlzPfo1ywk9m2oJ9ucMqA==} engines: {node: '>=v14'} - dependencies: - '@commitlint/config-validator': 17.1.0 - '@commitlint/execute-rule': 17.0.0 - '@commitlint/resolve-extends': 17.1.0 - '@commitlint/types': 17.0.0 - '@types/node': 14.18.29 - chalk: 4.1.2 - cosmiconfig: 7.0.1 - cosmiconfig-typescript-loader: 4.1.0_3owiowz3ujipd4k6pbqn3n7oui - lodash: 4.17.21 - resolve-from: 5.0.0 - ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa - typescript: 4.8.3 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - dev: true - /@commitlint/message/17.0.0: - resolution: {integrity: sha512-LpcwYtN+lBlfZijHUdVr8aNFTVpHjuHI52BnfoV01TF7iSLnia0jttzpLkrLmI8HNQz6Vhr9UrxDWtKZiMGsBw==} + '@commitlint/message@17.8.1': + resolution: {integrity: sha512-6bYL1GUQsD6bLhTH3QQty8pVFoETfFQlMn2Nzmz3AOLqRVfNNtXBaSY0dhZ0dM6A2MEq4+2d7L/2LP8TjqGRkA==} engines: {node: '>=v14'} - dev: true - /@commitlint/parse/17.0.0: - resolution: {integrity: sha512-cKcpfTIQYDG1ywTIr5AG0RAiLBr1gudqEsmAGCTtj8ffDChbBRxm6xXs2nv7GvmJN7msOt7vOKleLvcMmRa1+A==} + '@commitlint/parse@17.8.1': + resolution: {integrity: sha512-/wLUickTo0rNpQgWwLPavTm7WbwkZoBy3X8PpkUmlSmQJyWQTj0m6bDjiykMaDt41qcUbfeFfaCvXfiR4EGnfw==} engines: {node: '>=v14'} - dependencies: - '@commitlint/types': 17.0.0 - conventional-changelog-angular: 5.0.13 - conventional-commits-parser: 3.2.4 - dev: true - /@commitlint/read/17.1.0: - resolution: {integrity: sha512-73BoFNBA/3Ozo2JQvGsE0J8SdrJAWGfZQRSHqvKaqgmY042Su4gXQLqvAzgr55S9DI1l9TiU/5WDuh8IE86d/g==} + '@commitlint/read@17.8.1': + resolution: {integrity: sha512-Fd55Oaz9irzBESPCdMd8vWWgxsW3OWR99wOntBDHgf9h7Y6OOHjWEdS9Xzen1GFndqgyoaFplQS5y7KZe0kO2w==} engines: {node: '>=v14'} - dependencies: - '@commitlint/top-level': 17.0.0 - '@commitlint/types': 17.0.0 - fs-extra: 10.1.0 - git-raw-commits: 2.0.11 - minimist: 1.2.6 - dev: true - /@commitlint/resolve-extends/17.1.0: - resolution: {integrity: sha512-jqKm00LJ59T0O8O4bH4oMa4XyJVEOK4GzH8Qye9XKji+Q1FxhZznxMV/bDLyYkzbTodBt9sL0WLql8wMtRTbqQ==} + '@commitlint/resolve-extends@17.8.1': + resolution: {integrity: sha512-W/ryRoQ0TSVXqJrx5SGkaYuAaE/BUontL1j1HsKckvM6e5ZaG0M9126zcwL6peKSuIetJi7E87PRQF8O86EW0Q==} engines: {node: '>=v14'} - dependencies: - '@commitlint/config-validator': 17.1.0 - '@commitlint/types': 17.0.0 - import-fresh: 3.3.0 - lodash: 4.17.21 - resolve-from: 5.0.0 - resolve-global: 1.0.0 - dev: true - /@commitlint/rules/17.0.0: - resolution: {integrity: sha512-45nIy3dERKXWpnwX9HeBzK5SepHwlDxdGBfmedXhL30fmFCkJOdxHyOJsh0+B0RaVsLGT01NELpfzJUmtpDwdQ==} + '@commitlint/rules@17.8.1': + resolution: {integrity: sha512-2b7OdVbN7MTAt9U0vKOYKCDsOvESVXxQmrvuVUZ0rGFMCrCPJWWP1GJ7f0lAypbDAhaGb8zqtdOr47192LBrIA==} engines: {node: '>=v14'} - dependencies: - '@commitlint/ensure': 17.0.0 - '@commitlint/message': 17.0.0 - '@commitlint/to-lines': 17.0.0 - '@commitlint/types': 17.0.0 - execa: 5.1.1 - dev: true - /@commitlint/to-lines/17.0.0: - resolution: {integrity: sha512-nEi4YEz04Rf2upFbpnEorG8iymyH7o9jYIVFBG1QdzebbIFET3ir+8kQvCZuBE5pKCtViE4XBUsRZz139uFrRQ==} + '@commitlint/to-lines@17.8.1': + resolution: {integrity: sha512-LE0jb8CuR/mj6xJyrIk8VLz03OEzXFgLdivBytoooKO5xLt5yalc8Ma5guTWobw998sbR3ogDd+2jed03CFmJA==} engines: {node: '>=v14'} - dev: true - /@commitlint/top-level/17.0.0: - resolution: {integrity: sha512-dZrEP1PBJvodNWYPOYiLWf6XZergdksKQaT6i1KSROLdjf5Ai0brLOv5/P+CPxBeoj3vBxK4Ax8H1Pg9t7sHIQ==} + '@commitlint/top-level@17.8.1': + resolution: {integrity: sha512-l6+Z6rrNf5p333SHfEte6r+WkOxGlWK4bLuZKbtf/2TXRN+qhrvn1XE63VhD8Oe9oIHQ7F7W1nG2k/TJFhx2yA==} engines: {node: '>=v14'} - dependencies: - find-up: 5.0.0 - dev: true - /@commitlint/types/17.0.0: - resolution: {integrity: sha512-hBAw6U+SkAT5h47zDMeOu3HSiD0SODw4Aq7rRNh1ceUmL7GyLKYhPbUvlRWqZ65XjBLPHZhFyQlRaPNz8qvUyQ==} + '@commitlint/types@17.8.1': + resolution: {integrity: sha512-PXDQXkAmiMEG162Bqdh9ChML/GJZo6vU+7F03ALKDK8zYc6SuAr47LjG7hGYRqUOz+WK0dU7bQ0xzuqFMdxzeQ==} engines: {node: '>=v14'} - dependencies: - chalk: 4.1.2 - dev: true - /@cspotcode/source-map-support/0.8.1: + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - /@csstools/normalize.css/12.0.0: - resolution: {integrity: sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==} + '@csstools/normalize.css@12.1.1': + resolution: {integrity: sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==} - /@csstools/postcss-cascade-layers/1.1.1_postcss@8.4.16: + '@csstools/postcss-cascade-layers@1.1.1': resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - '@csstools/selector-specificity': 2.0.2_pnx64jze6bptzcedy5bidi3zdi - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - /@csstools/postcss-color-function/1.1.1_postcss@8.4.16: + '@csstools/postcss-color-function@1.1.1': resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-font-format-keywords/1.0.1_postcss@8.4.16: + '@csstools/postcss-font-format-keywords@1.0.1': resolution: {integrity: sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-hwb-function/1.0.2_postcss@8.4.16: + '@csstools/postcss-hwb-function@1.0.2': resolution: {integrity: sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-ic-unit/1.0.1_postcss@8.4.16: + '@csstools/postcss-ic-unit@1.0.1': resolution: {integrity: sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-is-pseudo-class/2.0.7_postcss@8.4.16: + '@csstools/postcss-is-pseudo-class@2.0.7': resolution: {integrity: sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - '@csstools/selector-specificity': 2.0.2_pnx64jze6bptzcedy5bidi3zdi - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - /@csstools/postcss-nested-calc/1.0.0_postcss@8.4.16: + '@csstools/postcss-nested-calc@1.0.0': resolution: {integrity: sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-normalize-display-values/1.0.1_postcss@8.4.16: + '@csstools/postcss-normalize-display-values@1.0.1': resolution: {integrity: sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-oklab-function/1.1.1_postcss@8.4.16: + '@csstools/postcss-oklab-function@1.1.1': resolution: {integrity: sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-progressive-custom-properties/1.3.0_postcss@8.4.16: + '@csstools/postcss-progressive-custom-properties@1.3.0': resolution: {integrity: sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.3 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-stepped-value-functions/1.0.1_postcss@8.4.16: + '@csstools/postcss-stepped-value-functions@1.0.1': resolution: {integrity: sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-text-decoration-shorthand/1.0.0_postcss@8.4.16: + '@csstools/postcss-text-decoration-shorthand@1.0.0': resolution: {integrity: sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-trigonometric-functions/1.0.2_postcss@8.4.16: + '@csstools/postcss-trigonometric-functions@1.0.2': resolution: {integrity: sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==} engines: {node: ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /@csstools/postcss-unset-value/1.0.2_postcss@8.4.16: + '@csstools/postcss-unset-value@1.0.2': resolution: {integrity: sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==} engines: {node: ^12 || ^14 || >=16} peerDependencies: postcss: ^8.2 - dependencies: - postcss: 8.4.16 - /@csstools/selector-specificity/2.0.2_pnx64jze6bptzcedy5bidi3zdi: - resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==} - engines: {node: ^12 || ^14 || >=16} + '@csstools/selector-specificity@2.2.0': + resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} + engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: ^8.2 postcss-selector-parser: ^6.0.10 - dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - /@eslint/eslintrc/1.3.2: - resolution: {integrity: sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==} + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.4.0 - globals: 13.17.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - /@fullhuman/postcss-purgecss/4.1.3_postcss@8.4.16: + '@fullhuman/postcss-purgecss@4.1.3': resolution: {integrity: sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw==} peerDependencies: postcss: ^8.0.0 - dependencies: - postcss: 8.4.16 - purgecss: 4.1.3 - dev: true - /@humanwhocodes/config-array/0.10.4: - resolution: {integrity: sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - /@humanwhocodes/gitignore-to-minimatch/1.0.2: - resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + deprecated: Use @eslint/config-array instead - /@humanwhocodes/module-importer/1.0.1: + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - /@humanwhocodes/object-schema/1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead - /@hutson/parse-repository-url/3.0.2: - resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} - engines: {node: '>=6.9.0'} - dev: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} - /@istanbuljs/load-nyc-config/1.1.0: + '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.1 - resolve-from: 5.0.0 - /@istanbuljs/schema/0.1.3: + '@istanbuljs/schema@0.1.3': resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - /@jest/console/27.5.1: + '@jest/console@27.5.1': resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/node': 16.11.59 - chalk: 4.1.2 - jest-message-util: 27.5.1 - jest-util: 27.5.1 - slash: 3.0.0 - /@jest/console/28.1.3: + '@jest/console@28.1.3': resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/types': 28.1.3 - '@types/node': 16.11.59 - chalk: 4.1.2 - jest-message-util: 28.1.3 - jest-util: 28.1.3 - slash: 3.0.0 - /@jest/core/27.5.1: + '@jest/core@27.5.1': resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} peerDependencies: @@ -1833,71 +1270,20 @@ packages: peerDependenciesMeta: node-notifier: optional: true - dependencies: - '@jest/console': 27.5.1 - '@jest/reporters': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 16.11.59 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.8.1 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 27.5.1 - jest-config: 27.5.1 - jest-haste-map: 27.5.1 - jest-message-util: 27.5.1 - jest-regex-util: 27.5.1 - jest-resolve: 27.5.1 - jest-resolve-dependencies: 27.5.1 - jest-runner: 27.5.1 - jest-runtime: 27.5.1 - jest-snapshot: 27.5.1 - jest-util: 27.5.1 - jest-validate: 27.5.1 - jest-watcher: 27.5.1 - micromatch: 4.0.5 - rimraf: 3.0.2 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - bufferutil - - canvas - - supports-color - - ts-node - - utf-8-validate - /@jest/environment/27.5.1: + '@jest/environment@27.5.1': resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/fake-timers': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 16.11.59 - jest-mock: 27.5.1 - /@jest/fake-timers/27.5.1: + '@jest/fake-timers@27.5.1': resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@sinonjs/fake-timers': 8.1.0 - '@types/node': 16.11.59 - jest-message-util: 27.5.1 - jest-mock: 27.5.1 - jest-util: 27.5.1 - /@jest/globals/27.5.1: + '@jest/globals@27.5.1': resolution: {integrity: sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/environment': 27.5.1 - '@jest/types': 27.5.1 - expect: 27.5.1 - /@jest/reporters/27.5.1: + '@jest/reporters@27.5.1': resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} peerDependencies: @@ -1905,211 +1291,157 @@ packages: peerDependenciesMeta: node-notifier: optional: true - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 27.5.1 - '@jest/test-result': 27.5.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 16.11.59 - chalk: 4.1.2 - collect-v8-coverage: 1.0.1 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-instrument: 5.2.0 - istanbul-lib-report: 3.0.0 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 - jest-haste-map: 27.5.1 - jest-resolve: 27.5.1 - jest-util: 27.5.1 - jest-worker: 27.5.1 - slash: 3.0.0 - source-map: 0.6.1 - string-length: 4.0.2 - terminal-link: 2.1.1 - v8-to-istanbul: 8.1.1 - transitivePeerDependencies: - - supports-color - /@jest/schemas/28.1.3: + '@jest/schemas@28.1.3': resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@sinclair/typebox': 0.24.42 - /@jest/source-map/27.5.1: + '@jest/source-map@27.5.1': resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - callsites: 3.1.0 - graceful-fs: 4.2.10 - source-map: 0.6.1 - /@jest/test-result/27.5.1: + '@jest/test-result@27.5.1': resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/console': 27.5.1 - '@jest/types': 27.5.1 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 - /@jest/test-result/28.1.3: + '@jest/test-result@28.1.3': resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/console': 28.1.3 - '@jest/types': 28.1.3 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.1 - /@jest/test-sequencer/27.5.1: + '@jest/test-sequencer@27.5.1': resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/test-result': 27.5.1 - graceful-fs: 4.2.10 - jest-haste-map: 27.5.1 - jest-runtime: 27.5.1 - transitivePeerDependencies: - - supports-color - /@jest/transform/27.5.1: + '@jest/transform@27.5.1': resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/core': 7.19.1 - '@jest/types': 27.5.1 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 1.8.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 - jest-haste-map: 27.5.1 - jest-regex-util: 27.5.1 - jest-util: 27.5.1 - micromatch: 4.0.5 - pirates: 4.0.5 - slash: 3.0.0 - source-map: 0.6.1 - write-file-atomic: 3.0.3 - transitivePeerDependencies: - - supports-color - /@jest/types/24.9.0: + '@jest/types@24.9.0': resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} engines: {node: '>= 6'} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 1.1.2 - '@types/yargs': 13.0.12 - dev: false - /@jest/types/27.5.1: + '@jest/types@27.5.1': resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 16.11.59 - '@types/yargs': 16.0.4 - chalk: 4.1.2 - /@jest/types/28.1.3: + '@jest/types@28.1.3': resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} - dependencies: - '@jest/schemas': 28.1.3 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 16.11.59 - '@types/yargs': 17.0.12 - chalk: 4.1.2 - - /@jridgewell/gen-mapping/0.1.1: - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - /@jridgewell/gen-mapping/0.3.2: - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.15 - /@jridgewell/resolve-uri/3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - /@jridgewell/set-array/1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - /@jridgewell/source-map/0.3.2: - resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} - dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.15 + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - /@jridgewell/sourcemap-codec/1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - /@jridgewell/trace-mapping/0.3.15: - resolution: {integrity: sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - /@jridgewell/trace-mapping/0.3.9: + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - /@leichtgewicht/ip-codec/2.0.4: - resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@lezer/common@1.2.3': + resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} + + '@lezer/cpp@1.1.2': + resolution: {integrity: sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==} + + '@lezer/css@1.1.9': + resolution: {integrity: sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==} + + '@lezer/go@1.0.0': + resolution: {integrity: sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==} + + '@lezer/highlight@1.2.1': + resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + + '@lezer/html@1.3.10': + resolution: {integrity: sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.4.21': + resolution: {integrity: sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==} + + '@lezer/json@1.0.2': + resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==} + + '@lezer/lr@1.4.2': + resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + + '@lezer/markdown@1.3.2': + resolution: {integrity: sha512-Wu7B6VnrKTbBEohqa63h5vxXjiC4pO5ZQJ/TDbhJxPQaaIoRD/6UVDhSDtVsCwVZV12vvN9KxuLL3ATMnlG0oQ==} + + '@lezer/php@1.0.2': + resolution: {integrity: sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==} - /@nicolo-ribaudo/eslint-scope-5-internals/5.1.1-v1: + '@lezer/python@1.1.15': + resolution: {integrity: sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.0.7': + resolution: {integrity: sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==} + + '@lezer/xml@1.0.5': + resolution: {integrity: sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==} + + '@lezer/yaml@1.0.3': + resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} - dependencies: - eslint-scope: 5.1.1 - /@nodelib/fs.scandir/2.1.5: + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - /@nodelib/fs.stat/2.0.5: + '@nodelib/fs.stat@2.0.5': resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - /@nodelib/fs.walk/1.2.8: + '@nodelib/fs.walk@1.2.8': resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - /@pmmmwh/react-refresh-webpack-plugin/0.5.7_prxwy2zxcolvdag5hfkyuqbcze: - resolution: {integrity: sha512-bcKCAzF0DV2IIROp9ZHkRJa6O4jy7NlnHdWL3GmcUxYWNjLXkK5kfELELwEfSP5hXPfVL/qOGMAROuMQb9GG8Q==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@pmmmwh/react-refresh-webpack-plugin@0.5.15': + resolution: {integrity: sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==} engines: {node: '>= 10.13'} peerDependencies: '@types/webpack': 4.x || 5.x react-refresh: '>=0.10.0 <1.0.0' sockjs-client: ^1.4.0 - type-fest: '>=0.17.0 <3.0.0' + type-fest: '>=0.17.0 <5.0.0' webpack: '>=4.43.0 <6.0.0' - webpack-dev-server: 3.x || 4.x + webpack-dev-server: 3.x || 4.x || 5.x webpack-hot-middleware: 2.x webpack-plugin-serve: 0.x || 1.x peerDependenciesMeta: @@ -2125,66 +1457,33 @@ packages: optional: true webpack-plugin-serve: optional: true - dependencies: - ansi-html-community: 0.0.8 - common-path-prefix: 3.0.0 - core-js-pure: 3.25.2 - error-stack-parser: 2.1.4 - find-up: 5.0.0 - html-entities: 2.3.3 - loader-utils: 2.0.2 - react-refresh: 0.11.0 - schema-utils: 3.1.1 - source-map: 0.7.4 - webpack: 5.74.0 - webpack-dev-server: 4.11.1_webpack@5.74.0 - /@popperjs/core/2.11.6: - resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - /@react-aria/ssr/3.3.0_react@18.2.0: - resolution: {integrity: sha512-yNqUDuOVZIUGP81R87BJVi/ZUZp/nYOBXbPsRe7oltJOfErQZD+UezMpw4vM2KRz18cURffvmC8tJ6JTeyDtaQ==} + '@react-aria/ssr@3.9.7': + resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} + engines: {node: '>= 12'} peerDependencies: - react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 - dependencies: - '@babel/runtime': 7.19.0 - react: 18.2.0 - dev: false + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - /@remix-run/router/1.0.0: - resolution: {integrity: sha512-SCR1cxRSMNKjaVYptCzBApPDqGwa3FGdjVHc+rOToocNPHQdIYLZBfv/3f+KvYuXDkUGVIW9IAzmPNZDRL1I4A==} - engines: {node: '>=14'} - dev: false + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' - /@restart/hooks/0.4.7_react@18.2.0: - resolution: {integrity: sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==} + '@restart/hooks@0.5.0': + resolution: {integrity: sha512-wS+h6IusJCPjTkmOOrRZxIPICD/mtFA3PRZviutoM23/b7akyDGfZF/WS+nIFk27u7JDhPE2+0GBdZxjSqHZkg==} peerDependencies: react: '>=16.8.0' - dependencies: - dequal: 2.0.3 - react: 18.2.0 - dev: false - /@restart/ui/1.4.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-5dDj5uDzUgK1iijWPRg6AnxjkHM04XhTQDJirM1h/8tIc7KyLtF9YyjcCpNEn259hPMXswpkfXKNgiag0skPFg==} + '@restart/ui@1.9.1': + resolution: {integrity: sha512-qghR21ynHiUrpcIkKCoKYB+3rJtezY5Y7ikrwradCL+7hZHdQ2Ozc5ffxtpmpahoAGgc31gyXaSx2sXXaThmqA==} peerDependencies: react: '>=16.14.0' react-dom: '>=16.14.0' - dependencies: - '@babel/runtime': 7.19.0 - '@popperjs/core': 2.11.6 - '@react-aria/ssr': 3.3.0_react@18.2.0 - '@restart/hooks': 0.4.7_react@18.2.0 - '@types/warning': 3.0.0 - dequal: 2.0.3 - dom-helpers: 5.2.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - uncontrollable: 7.2.1_react@18.2.0 - warning: 4.0.3 - dev: false - /@rollup/plugin-babel/5.3.1_qjhfxcwn2glzcb5646tzyg45bq: + '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} peerDependencies: @@ -2194,511 +1493,341 @@ packages: peerDependenciesMeta: '@types/babel__core': optional: true - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-module-imports': 7.18.6 - '@rollup/pluginutils': 3.1.0_rollup@2.79.0 - rollup: 2.79.0 - /@rollup/plugin-node-resolve/11.2.1_rollup@2.79.0: + '@rollup/plugin-node-resolve@11.2.1': resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} engines: {node: '>= 10.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0 - dependencies: - '@rollup/pluginutils': 3.1.0_rollup@2.79.0 - '@types/resolve': 1.17.1 - builtin-modules: 3.3.0 - deepmerge: 4.2.2 - is-module: 1.0.0 - resolve: 1.22.1 - rollup: 2.79.0 - /@rollup/plugin-replace/2.4.2_rollup@2.79.0: + '@rollup/plugin-replace@2.4.2': resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} peerDependencies: rollup: ^1.20.0 || ^2.0.0 - dependencies: - '@rollup/pluginutils': 3.1.0_rollup@2.79.0 - magic-string: 0.25.9 - rollup: 2.79.0 - /@rollup/pluginutils/3.1.0_rollup@2.79.0: + '@rollup/pluginutils@3.1.0': resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0 - dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.1 - rollup: 2.79.0 - /@rushstack/eslint-patch/1.2.0: - resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - /@sinclair/typebox/0.24.42: - resolution: {integrity: sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw==} + '@rushstack/eslint-patch@1.10.4': + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} - /@sinonjs/commons/1.8.3: - resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==} - dependencies: - type-detect: 4.0.8 + '@sinclair/typebox@0.24.51': + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - /@sinonjs/fake-timers/8.1.0: + '@sinonjs/commons@1.8.6': + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + + '@sinonjs/fake-timers@8.1.0': resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} - dependencies: - '@sinonjs/commons': 1.8.3 - /@surma/rollup-plugin-off-main-thread/2.2.3: + '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} - dependencies: - ejs: 3.1.8 - json5: 2.2.1 - magic-string: 0.25.9 - string.prototype.matchall: 4.0.7 - /@svgr/babel-plugin-add-jsx-attribute/5.4.0: + '@svgr/babel-plugin-add-jsx-attribute@5.4.0': resolution: {integrity: sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==} engines: {node: '>=10'} - /@svgr/babel-plugin-remove-jsx-attribute/5.4.0: + '@svgr/babel-plugin-remove-jsx-attribute@5.4.0': resolution: {integrity: sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==} engines: {node: '>=10'} - /@svgr/babel-plugin-remove-jsx-empty-expression/5.0.1: + '@svgr/babel-plugin-remove-jsx-empty-expression@5.0.1': resolution: {integrity: sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==} engines: {node: '>=10'} - /@svgr/babel-plugin-replace-jsx-attribute-value/5.0.1: + '@svgr/babel-plugin-replace-jsx-attribute-value@5.0.1': resolution: {integrity: sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==} engines: {node: '>=10'} - /@svgr/babel-plugin-svg-dynamic-title/5.4.0: + '@svgr/babel-plugin-svg-dynamic-title@5.4.0': resolution: {integrity: sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==} engines: {node: '>=10'} - /@svgr/babel-plugin-svg-em-dimensions/5.4.0: + '@svgr/babel-plugin-svg-em-dimensions@5.4.0': resolution: {integrity: sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==} engines: {node: '>=10'} - /@svgr/babel-plugin-transform-react-native-svg/5.4.0: + '@svgr/babel-plugin-transform-react-native-svg@5.4.0': resolution: {integrity: sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==} engines: {node: '>=10'} - /@svgr/babel-plugin-transform-svg-component/5.5.0: + '@svgr/babel-plugin-transform-svg-component@5.5.0': resolution: {integrity: sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==} engines: {node: '>=10'} - /@svgr/babel-preset/5.5.0: + '@svgr/babel-preset@5.5.0': resolution: {integrity: sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==} engines: {node: '>=10'} - dependencies: - '@svgr/babel-plugin-add-jsx-attribute': 5.4.0 - '@svgr/babel-plugin-remove-jsx-attribute': 5.4.0 - '@svgr/babel-plugin-remove-jsx-empty-expression': 5.0.1 - '@svgr/babel-plugin-replace-jsx-attribute-value': 5.0.1 - '@svgr/babel-plugin-svg-dynamic-title': 5.4.0 - '@svgr/babel-plugin-svg-em-dimensions': 5.4.0 - '@svgr/babel-plugin-transform-react-native-svg': 5.4.0 - '@svgr/babel-plugin-transform-svg-component': 5.5.0 - /@svgr/core/5.5.0: + '@svgr/core@5.5.0': resolution: {integrity: sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==} engines: {node: '>=10'} - dependencies: - '@svgr/plugin-jsx': 5.5.0 - camelcase: 6.3.0 - cosmiconfig: 7.0.1 - transitivePeerDependencies: - - supports-color - /@svgr/hast-util-to-babel-ast/5.5.0: + '@svgr/hast-util-to-babel-ast@5.5.0': resolution: {integrity: sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==} engines: {node: '>=10'} - dependencies: - '@babel/types': 7.19.0 - /@svgr/plugin-jsx/5.5.0: + '@svgr/plugin-jsx@5.5.0': resolution: {integrity: sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==} engines: {node: '>=10'} - dependencies: - '@babel/core': 7.19.1 - '@svgr/babel-preset': 5.5.0 - '@svgr/hast-util-to-babel-ast': 5.5.0 - svg-parser: 2.0.4 - transitivePeerDependencies: - - supports-color - /@svgr/plugin-svgo/5.5.0: + '@svgr/plugin-svgo@5.5.0': resolution: {integrity: sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==} engines: {node: '>=10'} - dependencies: - cosmiconfig: 7.0.1 - deepmerge: 4.2.2 - svgo: 1.3.2 - /@svgr/webpack/5.5.0: + '@svgr/webpack@5.5.0': resolution: {integrity: sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==} engines: {node: '>=10'} - dependencies: - '@babel/core': 7.19.1 - '@babel/plugin-transform-react-constant-elements': 7.18.12_@babel+core@7.19.1 - '@babel/preset-env': 7.19.1_@babel+core@7.19.1 - '@babel/preset-react': 7.18.6_@babel+core@7.19.1 - '@svgr/core': 5.5.0 - '@svgr/plugin-jsx': 5.5.0 - '@svgr/plugin-svgo': 5.5.0 - loader-utils: 2.0.2 - transitivePeerDependencies: - - supports-color - /@testing-library/dom/8.18.1: - resolution: {integrity: sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} engines: {node: '>=12'} - dependencies: - '@babel/code-frame': 7.18.6 - '@babel/runtime': 7.19.0 - '@types/aria-query': 4.2.2 - aria-query: 5.0.2 - chalk: 4.1.2 - dom-accessibility-api: 0.5.14 - lz-string: 1.4.4 - pretty-format: 27.5.1 - dev: true - /@testing-library/jest-dom/4.2.4: + '@testing-library/jest-dom@4.2.4': resolution: {integrity: sha512-j31Bn0rQo12fhCWOUWy9fl7wtqkp7In/YP2p5ZFyRuiiB9Qs3g+hS4gAmDWONbAHcRmVooNJ5eOHQDCOmUFXHg==} engines: {node: '>=8', npm: '>=6'} - dependencies: - '@babel/runtime': 7.19.0 - chalk: 2.4.2 - css: 2.2.4 - css.escape: 1.5.1 - jest-diff: 24.9.0 - jest-matcher-utils: 24.9.0 - lodash: 4.17.21 - pretty-format: 24.9.0 - redent: 3.0.0 - dev: false - /@testing-library/react/13.4.0_biqbaboplfbrettd7655fr4n2y: + '@testing-library/react@13.4.0': resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} engines: {node: '>=12'} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - dependencies: - '@babel/runtime': 7.19.0 - '@testing-library/dom': 8.18.1 - '@types/react-dom': 18.0.6 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: true - /@testing-library/user-event/13.5.0_znccgeejomvff3jrsk3ljovfpu: + '@testing-library/user-event@13.5.0': resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} engines: {node: '>=10', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' - dependencies: - '@babel/runtime': 7.19.0 - '@testing-library/dom': 8.18.1 - dev: true - /@tootallnate/once/1.1.2: + '@tootallnate/once@1.1.2': resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} - /@trysound/sax/0.2.0: + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - /@tsconfig/node10/1.0.9: - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} - dev: true + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - /@tsconfig/node12/1.0.11: + '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true - /@tsconfig/node14/1.0.3: + '@tsconfig/node14@1.0.3': resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true - /@tsconfig/node16/1.0.3: - resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} - dev: true + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - /@types/aria-query/4.2.2: - resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} - dev: true + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - /@types/babel__core/7.1.19: - resolution: {integrity: sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==} - dependencies: - '@babel/parser': 7.19.1 - '@babel/types': 7.19.0 - '@types/babel__generator': 7.6.4 - '@types/babel__template': 7.4.1 - '@types/babel__traverse': 7.18.1 + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - /@types/babel__generator/7.6.4: - resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} - dependencies: - '@babel/types': 7.19.0 + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - /@types/babel__template/7.4.1: - resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} - dependencies: - '@babel/parser': 7.19.1 - '@babel/types': 7.19.0 + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - /@types/babel__traverse/7.18.1: - resolution: {integrity: sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA==} - dependencies: - '@babel/types': 7.19.0 + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - /@types/body-parser/1.19.2: - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - dependencies: - '@types/connect': 3.4.35 - '@types/node': 16.11.59 + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - /@types/bonjour/3.5.10: - resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} - dependencies: - '@types/node': 16.11.59 + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} - /@types/connect-history-api-fallback/1.3.5: - resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} - dependencies: - '@types/express-serve-static-core': 4.17.31 - '@types/node': 16.11.59 + '@types/color-convert@2.0.4': + resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==} - /@types/connect/3.4.35: - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - dependencies: - '@types/node': 16.11.59 + '@types/color-name@1.1.5': + resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==} - /@types/eslint-scope/3.7.4: - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} - dependencies: - '@types/eslint': 8.4.6 - '@types/estree': 0.0.51 + '@types/color@3.0.6': + resolution: {integrity: sha512-NMiNcZFRUAiUUCCf7zkAelY8eV3aKqfbzyFQlXpPIEeoNDbsEHGpb854V3gzTsGKYj830I5zPuOwU/TP5/cW6A==} - /@types/eslint/8.4.6: - resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==} - dependencies: - '@types/estree': 1.0.0 - '@types/json-schema': 7.0.11 + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} - /@types/estree/0.0.39: + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/dompurify@2.4.0': + resolution: {integrity: sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@8.56.12': + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - /@types/estree/0.0.51: - resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - /@types/estree/1.0.0: - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} - /@types/express-serve-static-core/4.17.31: - resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} - dependencies: - '@types/node': 16.11.59 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 + '@types/express-serve-static-core@5.0.2': + resolution: {integrity: sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==} - /@types/express/4.17.14: - resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.31 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.0 + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - /@types/graceful-fs/4.1.5: - resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} - dependencies: - '@types/node': 16.11.59 + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - /@types/html-minifier-terser/6.1.0: + '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - /@types/http-proxy/1.17.9: - resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==} - dependencies: - '@types/node': 16.11.59 + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - /@types/istanbul-lib-coverage/2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + '@types/http-proxy@1.17.15': + resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} - /@types/istanbul-lib-report/3.0.0: - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - /@types/istanbul-reports/1.1.2: + '@types/istanbul-reports@1.1.2': resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-lib-report': 3.0.0 - dev: false - /@types/istanbul-reports/3.0.1: - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - dependencies: - '@types/istanbul-lib-report': 3.0.0 + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - /@types/jest/27.5.2: + '@types/jest@27.5.2': resolution: {integrity: sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==} - dependencies: - jest-matcher-utils: 27.5.1 - pretty-format: 27.5.1 - dev: true - - /@types/js-cookie/2.2.7: - resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} - dev: false - /@types/json-schema/7.0.11: - resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - /@types/json5/0.0.29: + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - /@types/lodash/4.14.185: - resolution: {integrity: sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==} - dev: true + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} - /@types/marked/4.0.7: - resolution: {integrity: sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==} - dev: true + '@types/marked@4.3.2': + resolution: {integrity: sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==} - /@types/mime/3.0.1: - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - /@types/minimist/1.2.2: - resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} - dev: true + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - /@types/node/14.18.29: - resolution: {integrity: sha512-LhF+9fbIX4iPzhsRLpK5H7iPdvW8L4IwGciXQIOEcuF62+9nw/VQVsOViAOOGxY3OlOKGLFv0sWwJXdwQeTn6A==} - dev: true + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - /@types/node/16.11.59: - resolution: {integrity: sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==} + '@types/node@16.18.121': + resolution: {integrity: sha512-Gk/pOy8H0cvX8qNrwzElYIECpcUn87w4EAEFXFvPJ8qsP9QR/YqukUORSy0zmyDyvdo149idPpy4W6iC5aSbQA==} - /@types/normalize-package-data/2.4.1: - resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - dev: true + '@types/node@20.5.1': + resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} - /@types/parse-json/4.0.0: - resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - /@types/prettier/2.7.0: - resolution: {integrity: sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - /@types/prop-types/15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + '@types/prettier@2.7.3': + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} - /@types/q/1.5.5: - resolution: {integrity: sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==} + '@types/prop-types@15.7.14': + resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} - /@types/qs/6.9.7: - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + '@types/q@1.5.8': + resolution: {integrity: sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==} - /@types/range-parser/1.2.4: - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + '@types/qs@6.9.17': + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} - /@types/react-dom/18.0.6: - resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==} - dependencies: - '@types/react': 18.0.20 - dev: true + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - /@types/react-helmet/6.1.5: - resolution: {integrity: sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==} - dependencies: - '@types/react': 18.0.20 - dev: true + '@types/react-dom@18.3.5': + resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==} + peerDependencies: + '@types/react': ^18.0.0 - /@types/react-transition-group/4.4.5: - resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==} - dependencies: - '@types/react': 18.0.20 - dev: false + '@types/react-transition-group@4.4.11': + resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} - /@types/react/18.0.20: - resolution: {integrity: sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 + '@types/react@18.3.16': + resolution: {integrity: sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==} - /@types/resolve/1.17.1: + '@types/resolve@1.17.1': resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} - dependencies: - '@types/node': 16.11.59 - /@types/retry/0.12.0: + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - /@types/scheduler/0.16.2: - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - /@types/serve-index/1.9.1: - resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} - dependencies: - '@types/express': 4.17.14 + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - /@types/serve-static/1.15.0: - resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} - dependencies: - '@types/mime': 3.0.1 - '@types/node': 16.11.59 + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} - /@types/sockjs/0.3.33: - resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} - dependencies: - '@types/node': 16.11.59 + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - /@types/stack-utils/2.0.1: - resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} - /@types/trusted-types/2.0.2: - resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - /@types/warning/3.0.0: - resolution: {integrity: sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==} - dev: false + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - /@types/ws/8.5.3: - resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} - dependencies: - '@types/node': 16.11.59 + '@types/warning@3.0.3': + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} - /@types/yargs-parser/21.0.0: - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} - /@types/yargs/13.0.12: + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@13.0.12': resolution: {integrity: sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==} - dependencies: - '@types/yargs-parser': 21.0.0 - dev: false - /@types/yargs/16.0.4: - resolution: {integrity: sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==} - dependencies: - '@types/yargs-parser': 21.0.0 + '@types/yargs@16.0.9': + resolution: {integrity: sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==} - /@types/yargs/17.0.12: - resolution: {integrity: sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ==} - dependencies: - '@types/yargs-parser': 21.0.0 + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - /@typescript-eslint/eslint-plugin/5.38.0_wsb62dxj2oqwgas4kadjymcmry: - resolution: {integrity: sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==} + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -2707,35 +1836,26 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/scope-manager': 5.38.0 - '@typescript-eslint/type-utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - debug: 4.3.4 - eslint: 8.23.1 - ignore: 5.2.0 - regexpp: 3.2.0 - semver: 7.3.7 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 - transitivePeerDependencies: - - supports-color - /@typescript-eslint/experimental-utils/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-kzXBRfvGlicgGk4CYuRUqKvwc2s3wHXNssUWWJU18bhMRxriFm3BZWyQ6vEHBRpEIMKB6b7MIQHO+9lYlts19w==} + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/experimental-utils@5.62.0': + resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - eslint: 8.23.1 - transitivePeerDependencies: - - supports-color - - typescript - /@typescript-eslint/parser/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==} + '@typescript-eslint/parser@5.62.0': + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2743,25 +1863,27 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/scope-manager': 5.38.0 - '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 - debug: 4.3.4 - eslint: 8.23.1 - typescript: 4.8.3 - transitivePeerDependencies: - - supports-color - /@typescript-eslint/scope-manager/5.38.0: - resolution: {integrity: sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==} + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/visitor-keys': 5.38.0 - /@typescript-eslint/type-utils/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==} + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -2769,1443 +1891,823 @@ packages: peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - debug: 4.3.4 - eslint: 8.23.1 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 - transitivePeerDependencies: - - supports-color - /@typescript-eslint/types/5.38.0: - resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==} + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3: - resolution: {integrity: sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==} + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true - dependencies: - '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/visitor-keys': 5.38.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.3.7 - tsutils: 3.21.0_typescript@4.8.3 - typescript: 4.8.3 - transitivePeerDependencies: - - supports-color - /@typescript-eslint/utils/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==} + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.11 - '@typescript-eslint/scope-manager': 5.38.0 - '@typescript-eslint/types': 5.38.0 - '@typescript-eslint/typescript-estree': 5.38.0_typescript@4.8.3 - eslint: 8.23.1 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0_eslint@8.23.1 - transitivePeerDependencies: - - supports-color - - typescript - /@typescript-eslint/visitor-keys/5.38.0: - resolution: {integrity: sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==} + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.38.0 - eslint-visitor-keys: 3.3.0 - /@webassemblyjs/ast/1.11.1: - resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} - dependencies: - '@webassemblyjs/helper-numbers': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} - /@webassemblyjs/floating-point-hex-parser/1.11.1: - resolution: {integrity: sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==} + '@ungap/structured-clone@1.2.1': + resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} - /@webassemblyjs/helper-api-error/1.11.1: - resolution: {integrity: sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - /@webassemblyjs/helper-buffer/1.11.1: - resolution: {integrity: sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==} + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - /@webassemblyjs/helper-numbers/1.11.1: - resolution: {integrity: sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==} - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 - '@xtuc/long': 4.2.2 + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - /@webassemblyjs/helper-wasm-bytecode/1.11.1: - resolution: {integrity: sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==} + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - /@webassemblyjs/helper-wasm-section/1.11.1: - resolution: {integrity: sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - /@webassemblyjs/ieee754/1.11.1: - resolution: {integrity: sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==} - dependencies: - '@xtuc/ieee754': 1.2.0 + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - /@webassemblyjs/leb128/1.11.1: - resolution: {integrity: sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==} - dependencies: - '@xtuc/long': 4.2.2 + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - /@webassemblyjs/utf8/1.11.1: - resolution: {integrity: sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==} + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - /@webassemblyjs/wasm-edit/1.11.1: - resolution: {integrity: sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/helper-wasm-section': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-opt': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - '@webassemblyjs/wast-printer': 1.11.1 + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - /@webassemblyjs/wasm-gen/1.11.1: - resolution: {integrity: sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - /@webassemblyjs/wasm-opt/1.11.1: - resolution: {integrity: sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-buffer': 1.11.1 - '@webassemblyjs/wasm-gen': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - /@webassemblyjs/wasm-parser/1.11.1: - resolution: {integrity: sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/helper-api-error': 1.11.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.1 - '@webassemblyjs/ieee754': 1.11.1 - '@webassemblyjs/leb128': 1.11.1 - '@webassemblyjs/utf8': 1.11.1 + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - /@webassemblyjs/wast-printer/1.11.1: - resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==} - dependencies: - '@webassemblyjs/ast': 1.11.1 - '@xtuc/long': 4.2.2 + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - /@xtuc/ieee754/1.2.0: + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - /@xtuc/long/4.2.2: + '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - /JSONStream/1.3.5: + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true - dependencies: - jsonparse: 1.3.1 - through: 2.3.8 - dev: true - /abab/2.0.6: + abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead - /accepts/1.3.8: + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - /acorn-globals/6.0.0: + acorn-globals@6.0.0: resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - - /acorn-import-assertions/1.8.0_acorn@8.8.0: - resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} - peerDependencies: - acorn: ^8 - dependencies: - acorn: 8.8.0 - /acorn-jsx/5.3.2_acorn@8.8.0: + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.8.0 - - /acorn-node/1.8.2: - resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} - dependencies: - acorn: 7.4.1 - acorn-walk: 7.2.0 - xtend: 4.0.2 - /acorn-walk/7.2.0: + acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} - /acorn-walk/8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - dev: true - /acorn/7.4.1: + acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true - /acorn/8.8.0: - resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true - /add-stream/1.0.0: - resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} - dev: true - - /address/1.2.1: - resolution: {integrity: sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==} + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - /adjust-sourcemap-loader/4.0.0: + adjust-sourcemap-loader@4.0.0: resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} engines: {node: '>=8.9'} - dependencies: - loader-utils: 2.0.2 - regex-parser: 2.2.11 - /agent-base/6.0.2: + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - /aggregate-error/3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: true - - /ahooks-v3-count/1.0.0: - resolution: {integrity: sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ==} - dev: false - - /ahooks/3.7.1_react@18.2.0: - resolution: {integrity: sha512-9fooKjhScNyJaIPnlWd13LkY1gQYqv3BqwSA9ynHg1ZUtDqAICuCRoedV97ylrEL6QqI4zeq3bO3lQxkfWVNcg==} - engines: {node: '>=8.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@types/js-cookie': 2.2.7 - ahooks-v3-count: 1.0.0 - dayjs: 1.11.5 - intersection-observer: 0.12.2 - js-cookie: 2.2.1 - lodash: 4.17.21 - react: 18.2.0 - resize-observer-polyfill: 1.5.1 - screenfull: 5.2.0 - dev: false - /ajv-formats/2.1.1: + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 peerDependenciesMeta: ajv: optional: true - dependencies: - ajv: 8.11.0 - /ajv-keywords/3.5.2_ajv@6.12.6: + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: ajv: ^6.9.1 - dependencies: - ajv: 6.12.6 - /ajv-keywords/5.1.0_ajv@8.11.0: + ajv-keywords@5.1.0: resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 - dependencies: - ajv: 8.11.0 - fast-deep-equal: 3.1.3 - /ajv/6.12.6: + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - /ajv/8.11.0: - resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - /ansi-escapes/4.3.2: + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - dependencies: - type-fest: 0.21.3 - /ansi-html-community/0.0.8: + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-html-community@0.0.8: resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} engines: {'0': node >= 0.8.0} hasBin: true - /ansi-regex/4.1.1: + ansi-html@0.0.9: + resolution: {integrity: sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} - dev: false - /ansi-regex/5.0.1: + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - /ansi-regex/6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} - /ansi-styles/3.2.1: + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - /ansi-styles/4.3.0: + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - /ansi-styles/5.2.0: + ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - /ansi-styles/6.1.1: - resolution: {integrity: sha512-qDOv24WjnYuL+wbwHdlsYZFy+cgPtrYw0Tn7GLORicQp9BkQLzrgI3Pm4VyR9ERZ41YTn7KlMPuL1n05WdZvmg==} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - dev: true - /anymatch/3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - /arg/4.1.3: + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: true - /arg/5.0.2: + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - /argparse/1.0.10: + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - /argparse/2.0.1: + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - /aria-query/4.2.2: - resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} - engines: {node: '>=6.0'} - dependencies: - '@babel/runtime': 7.19.0 - '@babel/runtime-corejs3': 7.19.1 + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} - /aria-query/5.0.2: - resolution: {integrity: sha512-eigU3vhqSO+Z8BKDnVLN/ompjhf3pYzecKXz8+whRy+9gZu8n1TCGfwzQUUPnqdHl9ax1Hr9031orZ+UOEYr7Q==} - engines: {node: '>=6.0'} - dev: true + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} - /array-flatten/1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} - /array-flatten/2.1.2: - resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - /array-ify/1.0.0: + array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - dev: true - /array-includes/3.1.5: - resolution: {integrity: sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==} + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 - get-intrinsic: 1.1.3 - is-string: 1.0.7 - /array-union/2.1.0: + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - /array.prototype.flat/1.3.0: - resolution: {integrity: sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==} + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 - es-shim-unscopables: 1.0.0 - /array.prototype.flatmap/1.3.0: - resolution: {integrity: sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==} + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 - es-shim-unscopables: 1.0.0 - /array.prototype.reduce/1.0.4: - resolution: {integrity: sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==} + array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 - es-array-method-boxes-properly: 1.0.0 - is-string: 1.0.7 - /arrify/1.0.1: + array.prototype.reduce@1.0.7: + resolution: {integrity: sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + + arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - dev: true - /asap/2.0.6: + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - /ast-types-flow/0.0.7: - resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} - - /astral-regex/2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: true + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - /async/3.2.4: - resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - /asynckit/0.4.0: + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /at-least-node/1.0.0: + at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - /atob/2.1.2: + atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} hasBin: true - dev: false - /autoprefixer/10.4.12_postcss@8.4.16: - resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - dependencies: - browserslist: 4.21.4 - caniuse-lite: 1.0.30001408 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.16 - postcss-value-parser: 4.2.0 - /axe-core/4.4.3: - resolution: {integrity: sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.2: + resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} - /axios/0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} - dependencies: - follow-redirects: 1.15.2 - form-data: 4.0.0 - transitivePeerDependencies: - - debug - dev: false + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} - /axobject-query/2.2.0: - resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} - /babel-jest/27.5.1_@babel+core@7.19.1: + babel-jest@27.5.1: resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} peerDependencies: '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.19.1 - '@jest/transform': 27.5.1 - '@jest/types': 27.5.1 - '@types/babel__core': 7.1.19 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 27.5.1_@babel+core@7.19.1 - chalk: 4.1.2 - graceful-fs: 4.2.10 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - /babel-loader/8.2.5_rhsdbzevgb5tizdhlla5jsbgyu: - resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==} + babel-loader@8.4.1: + resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} engines: {node: '>= 8.9'} peerDependencies: '@babel/core': ^7.0.0 webpack: '>=2' - dependencies: - '@babel/core': 7.19.1 - find-cache-dir: 3.3.2 - loader-utils: 2.0.2 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.74.0 - - /babel-plugin-dynamic-import-node/2.3.3: - resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} - dependencies: - object.assign: 4.1.4 - /babel-plugin-istanbul/6.1.1: + babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} - dependencies: - '@babel/helper-plugin-utils': 7.19.0 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.0 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - /babel-plugin-jest-hoist/27.5.1: + babel-plugin-jest-hoist@27.5.1: resolution: {integrity: sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/template': 7.18.10 - '@babel/types': 7.19.0 - '@types/babel__core': 7.1.19 - '@types/babel__traverse': 7.18.1 - /babel-plugin-macros/3.1.0: + babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} - dependencies: - '@babel/runtime': 7.19.0 - cosmiconfig: 7.0.1 - resolve: 1.22.1 - /babel-plugin-named-asset-import/0.3.8_@babel+core@7.19.1: + babel-plugin-named-asset-import@0.3.8: resolution: {integrity: sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==} peerDependencies: '@babel/core': ^7.1.0 - dependencies: - '@babel/core': 7.19.1 - /babel-plugin-polyfill-corejs2/0.3.3_@babel+core@7.19.1: - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + babel-plugin-polyfill-corejs2@0.4.12: + resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/compat-data': 7.19.1 - '@babel/core': 7.19.1 - '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.1 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /babel-plugin-polyfill-corejs3/0.6.0_@babel+core@7.19.1: - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + babel-plugin-polyfill-corejs3@0.10.6: + resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.1 - core-js-compat: 3.25.2 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /babel-plugin-polyfill-regenerator/0.4.1_@babel+core@7.19.1: - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + babel-plugin-polyfill-regenerator@0.6.3: + resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==} peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.19.1 - '@babel/helper-define-polyfill-provider': 0.3.3_@babel+core@7.19.1 - transitivePeerDependencies: - - supports-color + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - /babel-plugin-transform-react-remove-prop-types/0.4.24: + babel-plugin-transform-react-remove-prop-types@0.4.24: resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==} - /babel-preset-current-node-syntax/1.0.1_@babel+core@7.19.1: - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - '@babel/plugin-syntax-async-generators': 7.8.4_@babel+core@7.19.1 - '@babel/plugin-syntax-bigint': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.19.1 - '@babel/plugin-syntax-import-meta': 7.10.4_@babel+core@7.19.1 - '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.19.1 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.19.1 - '@babel/plugin-syntax-object-rest-spread': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-optional-chaining': 7.8.3_@babel+core@7.19.1 - '@babel/plugin-syntax-top-level-await': 7.14.5_@babel+core@7.19.1 - - /babel-preset-jest/27.5.1_@babel+core@7.19.1: + + babel-preset-jest@27.5.1: resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.19.1 - babel-plugin-jest-hoist: 27.5.1 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.1 - /babel-preset-react-app/10.0.1: + babel-preset-react-app@10.0.1: resolution: {integrity: sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==} - dependencies: - '@babel/core': 7.19.1 - '@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-decorators': 7.19.1_@babel+core@7.19.1 - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-numeric-separator': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-optional-chaining': 7.18.9_@babel+core@7.19.1 - '@babel/plugin-proposal-private-methods': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-proposal-private-property-in-object': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-flow-strip-types': 7.19.0_@babel+core@7.19.1 - '@babel/plugin-transform-react-display-name': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-runtime': 7.19.1_@babel+core@7.19.1 - '@babel/preset-env': 7.19.1_@babel+core@7.19.1 - '@babel/preset-react': 7.18.6_@babel+core@7.19.1 - '@babel/preset-typescript': 7.18.6_@babel+core@7.19.1 - '@babel/runtime': 7.19.0 - babel-plugin-macros: 3.1.0 - babel-plugin-transform-react-remove-prop-types: 0.4.24 - transitivePeerDependencies: - - supports-color - /balanced-match/1.0.2: + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - /base64-js/1.5.1: + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - /batch/0.6.1: + batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - /bfj/7.0.2: - resolution: {integrity: sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==} + bfj@7.1.0: + resolution: {integrity: sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==} engines: {node: '>= 8.0.0'} - dependencies: - bluebird: 3.7.2 - check-types: 11.1.2 - hoopy: 0.1.4 - tryer: 1.0.1 - /big.js/5.2.2: + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - /binary-extensions/2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - /bl/4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.0 - dev: true - - /bluebird/3.7.2: + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - /body-parser/1.20.0: - resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.4 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.10.3 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - /bonjour-service/1.0.14: - resolution: {integrity: sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==} - dependencies: - array-flatten: 2.1.2 - dns-equal: 1.0.0 - fast-deep-equal: 3.1.3 - multicast-dns: 7.2.5 + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} - /boolbase/1.0.0: + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - /bootstrap-icons/1.9.1: - resolution: {integrity: sha512-d4ZkO30MIkAhQ2nNRJqKXJVEQorALGbLWTuRxyCTJF96lRIV6imcgMehWGJUiJMJhglN0o2tqLIeDnMdiQEE9g==} - dev: false + bootstrap-icons@1.11.3: + resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==} - /bootstrap/5.2.1_@popperjs+core@2.11.6: - resolution: {integrity: sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==} + bootstrap@5.3.3: + resolution: {integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==} peerDependencies: - '@popperjs/core': ^2.11.6 - dependencies: - '@popperjs/core': 2.11.6 - dev: false + '@popperjs/core': ^2.11.8 - /brace-expansion/1.1.11: + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - /brace-expansion/2.0.1: + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - dependencies: - balanced-match: 1.0.2 - /braces/3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - /browser-process-hrtime/1.0.0: + browser-process-hrtime@1.0.0: resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} - /browserslist/4.21.4: - resolution: {integrity: sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==} + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - dependencies: - caniuse-lite: 1.0.30001408 - electron-to-chromium: 1.4.256 - node-releases: 2.0.6 - update-browserslist-db: 1.0.9_browserslist@4.21.4 - /bser/2.1.1: + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - dependencies: - node-int64: 0.4.0 - /buffer-from/1.1.2: + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - /buffer/5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - /builtin-modules/3.3.0: + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - /builtins/5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} - dependencies: - semver: 7.3.7 - dev: true - - /bytes/3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} - /bytes/3.1.2: + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - /cachedir/2.3.0: - resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} - engines: {node: '>=6'} - dev: true + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} - /call-bind/1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.1.3 + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} - /callsites/3.1.0: + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - /camel-case/4.1.2: + camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - dependencies: - pascal-case: 3.1.2 - tslib: 2.4.0 - /camelcase-css/2.0.1: + camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - /camelcase-keys/6.2.2: + camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - map-obj: 4.3.0 - quick-lru: 4.0.1 - dev: true - /camelcase/5.3.1: + camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - /camelcase/6.3.0: + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - /caniuse-api/3.0.0: + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - dependencies: - browserslist: 4.21.4 - caniuse-lite: 1.0.30001408 - lodash.memoize: 4.1.2 - lodash.uniq: 4.5.0 - /caniuse-lite/1.0.30001408: - resolution: {integrity: sha512-DdUCktgMSM+1ndk9EFMZcavsGszV7zxV9O7MtOHniTa/iyAIwJCF0dFVBdU9SijJbfh29hC9bCs07wu8pjnGJQ==} + caniuse-lite@1.0.30001687: + resolution: {integrity: sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==} - /case-sensitive-paths-webpack-plugin/2.4.0: + case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} - /chalk/2.4.2: + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - /chalk/4.1.2: + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - /char-regex/1.0.2: + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - /char-regex/2.0.1: - resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} + char-regex@2.0.2: + resolution: {integrity: sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==} engines: {node: '>=12.20'} - /chardet/0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true - - /check-types/11.1.2: - resolution: {integrity: sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==} + check-types@11.2.3: + resolution: {integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==} - /chokidar/3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - /chrome-trace-event/1.0.3: - resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - /ci-info/3.4.0: - resolution: {integrity: sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug==} + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} - /cjs-module-lexer/1.2.2: - resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} - /classnames/2.3.2: - resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} - dev: false + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - /clean-css/5.3.1: - resolution: {integrity: sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==} + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} - dependencies: - source-map: 0.6.1 - /clean-stack/2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: true + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} - /cli-cursor/3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 - dev: true + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} - /cli-spinners/2.7.0: - resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} - engines: {node: '>=6'} - dev: true + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - /cli-truncate/2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - dev: true + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - /cli-truncate/3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - slice-ansi: 5.0.0 - string-width: 5.1.2 - dev: true - - /cli-width/3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - dev: true - - /cliui/7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - /clone/1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} - /co/4.6.0: + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - /coa/2.0.2: + coa@2.0.2: resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==} engines: {node: '>= 4.0'} - dependencies: - '@types/q': 1.5.5 - chalk: 2.4.2 - q: 1.5.1 - /codemirror/5.65.0: - resolution: {integrity: sha512-gWEnHKEcz1Hyz7fsQWpK7P0sPI2/kSkRX2tc7DFA6TmZuDN75x/1ejnH/Pn8adYKrLEA1V2ww6L00GudHZbSKw==} - dev: false + codemirror@6.0.1: + resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} - /collect-v8-coverage/1.0.1: - resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} - /color-convert/1.9.3: + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - /color-convert/2.0.1: + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - /color-name/1.1.3: + color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - /color-name/1.1.4: + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - /colord/2.9.3: + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - /colorette/2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - /combined-stream/1.0.8: + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - /commander/2.20.3: + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - /commander/4.1.1: + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - dev: true - /commander/7.2.0: + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - /commander/8.3.0: + commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - /commander/9.4.0: - resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} - engines: {node: ^12.20.0 || >=14} - dev: true - - /commitizen/4.2.5: - resolution: {integrity: sha512-9sXju8Qrz1B4Tw7kC5KhnvwYQN88qs2zbiB8oyMsnXZyJ24PPGiNM3nHr73d32dnE3i8VJEXddBFIbOgYSEXtQ==} - engines: {node: '>= 12'} - hasBin: true - dependencies: - cachedir: 2.3.0 - cz-conventional-changelog: 3.3.0 - dedent: 0.7.0 - detect-indent: 6.1.0 - find-node-modules: 2.1.3 - find-root: 1.1.0 - fs-extra: 9.1.0 - glob: 7.2.3 - inquirer: 8.2.4 - is-utf8: 0.2.1 - lodash: 4.17.21 - minimist: 1.2.6 - strip-bom: 4.0.0 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - dev: true - - /common-path-prefix/3.0.0: - resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} - - /common-tags/1.8.2: + common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} - /commondir/1.0.1: + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - /compare-func/2.0.0: + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - dependencies: - array-ify: 1.0.0 - dot-prop: 5.3.0 - dev: true - /compressible/2.0.18: + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - /compression/1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + compression@1.7.5: + resolution: {integrity: sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==} engines: {node: '>= 0.8.0'} - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - /concat-map/0.0.1: + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - /confusing-browser-globals/1.0.11: + confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} - /connect-history-api-fallback/2.0.0: + connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} - /content-disposition/0.5.4: + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - /content-type/1.0.4: - resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - /conventional-changelog-angular/5.0.13: - resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} - engines: {node: '>=10'} - dependencies: - compare-func: 2.0.0 - q: 1.5.1 - dev: true - - /conventional-changelog-atom/2.0.8: - resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-cli/2.2.2: - resolution: {integrity: sha512-8grMV5Jo8S0kP3yoMeJxV2P5R6VJOqK72IiSV9t/4H5r/HiRqEBQ83bYGuz4Yzfdj4bjaAEhZN/FFbsFXr5bOA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - add-stream: 1.0.0 - conventional-changelog: 3.1.25 - lodash: 4.17.21 - meow: 8.1.2 - tempfile: 3.0.0 - dev: true - - /conventional-changelog-codemirror/2.0.8: - resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-conventionalcommits/4.6.3: - resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} - engines: {node: '>=10'} - dependencies: - compare-func: 2.0.0 - lodash: 4.17.21 - q: 1.5.1 - dev: true - - /conventional-changelog-conventionalcommits/5.0.0: - resolution: {integrity: sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==} - engines: {node: '>=10'} - dependencies: - compare-func: 2.0.0 - lodash: 4.17.21 - q: 1.5.1 - dev: true - - /conventional-changelog-core/4.2.4: - resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} - engines: {node: '>=10'} - dependencies: - add-stream: 1.0.0 - conventional-changelog-writer: 5.0.1 - conventional-commits-parser: 3.2.4 - dateformat: 3.0.3 - get-pkg-repo: 4.2.1 - git-raw-commits: 2.0.11 - git-remote-origin-url: 2.0.0 - git-semver-tags: 4.1.1 - lodash: 4.17.21 - normalize-package-data: 3.0.3 - q: 1.5.1 - read-pkg: 3.0.0 - read-pkg-up: 3.0.0 - through2: 4.0.2 - dev: true - - /conventional-changelog-ember/2.0.9: - resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-eslint/3.0.9: - resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-express/2.0.6: - resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-jquery/3.0.11: - resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} - engines: {node: '>=10'} - dependencies: - q: 1.5.1 - dev: true - - /conventional-changelog-jshint/2.0.9: - resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} - engines: {node: '>=10'} - dependencies: - compare-func: 2.0.0 - q: 1.5.1 - dev: true + conventional-changelog-angular@6.0.0: + resolution: {integrity: sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==} + engines: {node: '>=14'} - /conventional-changelog-preset-loader/2.3.4: - resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} - engines: {node: '>=10'} - dev: true + conventional-changelog-conventionalcommits@6.1.0: + resolution: {integrity: sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==} + engines: {node: '>=14'} - /conventional-changelog-writer/5.0.1: - resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} - engines: {node: '>=10'} + conventional-commits-parser@4.0.0: + resolution: {integrity: sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==} + engines: {node: '>=14'} hasBin: true - dependencies: - conventional-commits-filter: 2.0.7 - dateformat: 3.0.3 - handlebars: 4.7.7 - json-stringify-safe: 5.0.1 - lodash: 4.17.21 - meow: 8.1.2 - semver: 6.3.0 - split: 1.0.1 - through2: 4.0.2 - dev: true - - /conventional-changelog/3.1.25: - resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} - engines: {node: '>=10'} - dependencies: - conventional-changelog-angular: 5.0.13 - conventional-changelog-atom: 2.0.8 - conventional-changelog-codemirror: 2.0.8 - conventional-changelog-conventionalcommits: 4.6.3 - conventional-changelog-core: 4.2.4 - conventional-changelog-ember: 2.0.9 - conventional-changelog-eslint: 3.0.9 - conventional-changelog-express: 2.0.6 - conventional-changelog-jquery: 3.0.11 - conventional-changelog-jshint: 2.0.9 - conventional-changelog-preset-loader: 2.3.4 - dev: true - - /conventional-commit-types/3.0.0: - resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} - dev: true - - /conventional-commits-filter/2.0.7: - resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} - engines: {node: '>=10'} - dependencies: - lodash.ismatch: 4.4.0 - modify-values: 1.0.1 - dev: true - /conventional-commits-parser/3.2.4: - resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} - engines: {node: '>=10'} - hasBin: true - dependencies: - is-text-path: 1.0.1 - JSONStream: 1.3.5 - lodash: 4.17.21 - meow: 8.1.2 - split2: 3.2.2 - through2: 4.0.2 - dev: true + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - /convert-source-map/1.8.0: - resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==} - dependencies: - safe-buffer: 5.1.2 + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - /cookie-signature/1.0.6: + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - /cookie/0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} - /copy-to-clipboard/3.3.2: - resolution: {integrity: sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==} - dependencies: - toggle-selection: 1.0.6 - dev: false + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} - /core-js-compat/3.25.2: - resolution: {integrity: sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ==} - dependencies: - browserslist: 4.21.4 + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + core-js-compat@3.39.0: + resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} - /core-js-pure/3.25.2: - resolution: {integrity: sha512-ItD7YpW1cUB4jaqFLZXe1AXkyqIxz6GqPnsDV4uF4hVcWh/WAGIqSqw5p0/WdsILM0Xht9s3Koyw05R3K6RtiA==} - requiresBuild: true + core-js-pure@3.39.0: + resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==} - /core-js/3.25.2: - resolution: {integrity: sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A==} - requiresBuild: true + core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} - /core-util-is/1.0.3: + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - /cosmiconfig-typescript-loader/4.1.0_3owiowz3ujipd4k6pbqn3n7oui: - resolution: {integrity: sha512-HbWIuR5O+XO5Oj9SZ5bzgrD4nN+rfhrm2PMb0FVx+t+XIvC45n8F0oTNnztXtspWGw0i2IzHaUWFD5LzV1JB4A==} - engines: {node: '>=12', npm: '>=6'} + cosmiconfig-typescript-loader@4.4.0: + resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} + engines: {node: '>=v14.21.3'} peerDependencies: '@types/node': '*' cosmiconfig: '>=7' ts-node: '>=10' - typescript: '>=3' - dependencies: - '@types/node': 14.18.29 - cosmiconfig: 7.0.1 - ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa - typescript: 4.8.3 - dev: true + typescript: '>=4' - /cosmiconfig/6.0.0: + cosmiconfig@6.0.0: resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} engines: {node: '>=8'} - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - /cosmiconfig/7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - /create-require/1.1.1: + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true - /cross-fetch/3.1.5: - resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding - dev: false + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - /cross-spawn/7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - /crypto-random-string/2.0.0: + crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} - /css-blank-pseudo/3.0.3_postcss@8.4.16: + css-blank-pseudo@3.0.3: resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 - dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - /css-declaration-sorter/6.3.1_postcss@8.4.16: - resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} + css-declaration-sorter@6.4.1: + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 - dependencies: - postcss: 8.4.16 - /css-has-pseudo/3.0.4_postcss@8.4.16: + css-has-pseudo@3.0.4: resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 - dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - /css-loader/6.7.1_webpack@5.74.0: - resolution: {integrity: sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==} + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} engines: {node: '>= 12.13.0'} peerDependencies: + '@rspack/core': 0.x || 1.x webpack: ^5.0.0 - dependencies: - icss-utils: 5.1.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-modules-extract-imports: 3.0.0_postcss@8.4.16 - postcss-modules-local-by-default: 4.0.0_postcss@8.4.16 - postcss-modules-scope: 3.0.0_postcss@8.4.16 - postcss-modules-values: 4.0.0_postcss@8.4.16 - postcss-value-parser: 4.2.0 - semver: 7.3.7 - webpack: 5.74.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true - /css-minimizer-webpack-plugin/3.4.1_webpack@5.74.0: + css-minimizer-webpack-plugin@3.4.1: resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -4223,1208 +2725,8038 @@ packages: optional: true esbuild: optional: true - dependencies: - cssnano: 5.1.13_postcss@8.4.16 - jest-worker: 27.5.1 - postcss: 8.4.16 - schema-utils: 4.0.0 - serialize-javascript: 6.0.0 - source-map: 0.6.1 - webpack: 5.74.0 - /css-prefers-color-scheme/6.0.3_postcss@8.4.16: + css-prefers-color-scheme@6.0.3: resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} engines: {node: ^12 || ^14 || >=16} hasBin: true peerDependencies: postcss: ^8.4 - dependencies: - postcss: 8.4.16 - /css-select-base-adapter/0.1.1: + css-select-base-adapter@0.1.1: resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==} - /css-select/2.1.0: + css-select@2.1.0: resolution: {integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==} - dependencies: - boolbase: 1.0.0 - css-what: 3.4.2 - domutils: 1.7.0 - nth-check: 1.0.2 - /css-select/4.3.0: + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - /css-tree/1.0.0-alpha.37: + css-tree@1.0.0-alpha.37: resolution: {integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==} engines: {node: '>=8.0.0'} - dependencies: - mdn-data: 2.0.4 - source-map: 0.6.1 - /css-tree/1.1.3: + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - /css-what/3.4.2: + css-what@3.4.2: resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} engines: {node: '>= 6'} - /css-what/6.1.0: + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - /css.escape/1.5.1: + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - dev: false - /css/2.2.4: + css@2.2.4: resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==} - dependencies: - inherits: 2.0.4 - source-map: 0.6.1 - source-map-resolve: 0.5.3 - urix: 0.1.0 - dev: false - /cssdb/7.0.1: - resolution: {integrity: sha512-pT3nzyGM78poCKLAEy2zWIVX2hikq6dIrjuZzLV98MumBg+xMTNYfHx7paUlfiRTgg91O/vR889CIf+qiv79Rw==} + cssdb@7.11.2: + resolution: {integrity: sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==} - /cssesc/3.0.0: + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - /cssnano-preset-default/5.2.12_postcss@8.4.16: - resolution: {integrity: sha512-OyCBTZi+PXgylz9HAA5kHyoYhfGcYdwFmyaJzWnzxuGRtnMw/kR6ilW9XzlzlRAtB6PLT/r+prYgkef7hngFew==} + cssnano-preset-default@5.2.14: + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 - dependencies: - css-declaration-sorter: 6.3.1_postcss@8.4.16 - cssnano-utils: 3.1.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-calc: 8.2.4_postcss@8.4.16 - postcss-colormin: 5.3.0_postcss@8.4.16 - postcss-convert-values: 5.1.2_postcss@8.4.16 - postcss-discard-comments: 5.1.2_postcss@8.4.16 - postcss-discard-duplicates: 5.1.0_postcss@8.4.16 - postcss-discard-empty: 5.1.1_postcss@8.4.16 - postcss-discard-overridden: 5.1.0_postcss@8.4.16 - postcss-merge-longhand: 5.1.6_postcss@8.4.16 - postcss-merge-rules: 5.1.2_postcss@8.4.16 - postcss-minify-font-values: 5.1.0_postcss@8.4.16 - postcss-minify-gradients: 5.1.1_postcss@8.4.16 - postcss-minify-params: 5.1.3_postcss@8.4.16 - postcss-minify-selectors: 5.2.1_postcss@8.4.16 - postcss-normalize-charset: 5.1.0_postcss@8.4.16 - postcss-normalize-display-values: 5.1.0_postcss@8.4.16 - postcss-normalize-positions: 5.1.1_postcss@8.4.16 - postcss-normalize-repeat-style: 5.1.1_postcss@8.4.16 - postcss-normalize-string: 5.1.0_postcss@8.4.16 - postcss-normalize-timing-functions: 5.1.0_postcss@8.4.16 - postcss-normalize-unicode: 5.1.0_postcss@8.4.16 - postcss-normalize-url: 5.1.0_postcss@8.4.16 - postcss-normalize-whitespace: 5.1.1_postcss@8.4.16 - postcss-ordered-values: 5.1.3_postcss@8.4.16 - postcss-reduce-initial: 5.1.0_postcss@8.4.16 - postcss-reduce-transforms: 5.1.0_postcss@8.4.16 - postcss-svgo: 5.1.0_postcss@8.4.16 - postcss-unique-selectors: 5.1.1_postcss@8.4.16 - - /cssnano-utils/3.1.0_postcss@8.4.16: + + cssnano-utils@3.1.0: resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 - dependencies: - postcss: 8.4.16 - /cssnano/5.1.13_postcss@8.4.16: - resolution: {integrity: sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==} + cssnano@5.1.15: + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} engines: {node: ^10 || ^12 || >=14.0} peerDependencies: postcss: ^8.2.15 - dependencies: - cssnano-preset-default: 5.2.12_postcss@8.4.16 - lilconfig: 2.0.6 - postcss: 8.4.16 - yaml: 1.10.2 - /csso/4.2.0: + csso@4.2.0: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} - dependencies: - css-tree: 1.1.3 - /cssom/0.3.8: + cssom@0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - /cssom/0.4.4: + cssom@0.4.4: resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} - /cssstyle/2.3.0: + cssstyle@2.3.0: resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} engines: {node: '>=8'} - dependencies: - cssom: 0.3.8 - /csstype/3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + customize-cra@1.0.0: + resolution: {integrity: sha512-DbtaLuy59224U+xCiukkxSq8clq++MOtJ1Et7LED1fLszWe88EoblEYFBJ895sB1mC6B4uu3xPT/IjClELhMbA==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + + data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} + + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + detect-port-alt@1.1.6: + resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} + engines: {node: '>= 4.2.1'} + hasBin: true + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-sequences@24.9.0: + resolution: {integrity: sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==} + engines: {node: '>= 6'} + + diff-sequences@27.5.1: + resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + + dunder-proto@1.0.0: + resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.72: + resolution: {integrity: sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw==} + + emittery@0.10.2: + resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + engines: {node: '>=12'} + + emittery@0.8.1: + resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} + engines: {node: '>=10'} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-abstract@1.23.5: + resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} + engines: {node: '>= 0.4'} + + es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-iterator-helpers@1.2.0: + resolution: {integrity: sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-airbnb-base@15.0.0: + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + + eslint-config-airbnb-typescript@17.1.0: + resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 + '@typescript-eslint/parser': ^5.0.0 || ^6.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + + eslint-config-airbnb@19.0.4: + resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} + engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-react-app@7.0.1: + resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-standard-with-typescript@39.1.1: + resolution: {integrity: sha512-t6B5Ep8E4I18uuoYeYxINyqcXb2UbC0SOOTxRtBSt2JUs+EzeXbfe2oaiPs71AIdnoWhXDO2fYOHz8df3kV84A==} + deprecated: Please use eslint-config-love, instead. + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.4.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + typescript: '*' + + eslint-config-standard@17.1.0: + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-flowtype@8.0.3: + resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@babel/plugin-syntax-flow': ^7.14.5 + '@babel/plugin-transform-react-jsx': ^7.14.9 + eslint: ^8.1.0 + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jest@25.7.0: + resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-n@16.6.2: + resolution: {integrity: sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-promise@6.6.0: + resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.37.2: + resolution: {integrity: sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-testing-library@5.11.1: + resolution: {integrity: sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} + peerDependencies: + eslint: ^7.5.0 || ^8.0.0 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-webpack-plugin@3.2.0: + resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + webpack: ^5.0.0 + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@1.2.2: + resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==} + engines: {node: '>=0.4.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@27.5.1: + resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filesize@8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@6.5.3: + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + + form-data@3.0.2: + resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==} + engines: {node: '>= 6'} + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.2.5: + resolution: {integrity: sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==} + engines: {node: '>= 0.4'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + harmony-reflect@1.6.2: + resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==} + + has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hoopy@0.1.4: + resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} + engines: {node: '>= 6.0.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + + html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + + html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + html-webpack-plugin@5.6.3: + resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-middleware@2.0.7: + resolution: {integrity: sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18next@21.10.0: + resolution: {integrity: sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + + identity-obj-proxy@3.0.0: + resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} + engines: {node: '>=4'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.0.0: + resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.0: + resolution: {integrity: sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==} + engines: {node: '>= 0.4'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + + is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.0: + resolution: {integrity: sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.0: + resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.0: + resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-root@2.1.0: + resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} + engines: {node: '>=6'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.0: + resolution: {integrity: sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.0: + resolution: {integrity: sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==} + engines: {node: '>= 0.4'} + + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + + is-weakset@2.0.3: + resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + iterator.prototype@1.1.3: + resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + + jest-changed-files@27.5.1: + resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-circus@27.5.1: + resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-cli@27.5.1: + resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@27.5.1: + resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + ts-node: '>=9.0.0' + peerDependenciesMeta: + ts-node: + optional: true + + jest-diff@24.9.0: + resolution: {integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==} + engines: {node: '>= 6'} + + jest-diff@27.5.1: + resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-docblock@27.5.1: + resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-each@27.5.1: + resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-environment-jsdom@27.5.1: + resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-environment-node@27.5.1: + resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-get-type@24.9.0: + resolution: {integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==} + engines: {node: '>= 6'} + + jest-get-type@27.5.1: + resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-haste-map@27.5.1: + resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-jasmine2@27.5.1: + resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-leak-detector@27.5.1: + resolution: {integrity: sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-matcher-utils@24.9.0: + resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} + engines: {node: '>= 6'} + + jest-matcher-utils@27.5.1: + resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-message-util@27.5.1: + resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-message-util@28.1.3: + resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-mock@27.5.1: + resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@27.5.1: + resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-regex-util@28.0.2: + resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-resolve-dependencies@27.5.1: + resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-resolve@27.5.1: + resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-runner@27.5.1: + resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-runtime@27.5.1: + resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-serializer@27.5.1: + resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-snapshot@27.5.1: + resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-util@27.5.1: + resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-util@28.1.3: + resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-validate@27.5.1: + resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-watch-typeahead@1.1.0: + resolution: {integrity: sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + jest: ^27.0.0 || ^28.0.0 + + jest-watcher@27.5.1: + resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-watcher@28.1.3: + resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-worker@26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@28.1.3: + resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest@27.5.1: + resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + js-sha256@0.11.0: + resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonp@0.2.1: + resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsonpath@1.1.1: + resolution: {integrity: sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + launch-editor@2.9.1: + resolution: {integrity: sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.5.0: + resolution: {integrity: sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} + engines: {node: '>=18.0.0'} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + loader-utils@3.3.1: + resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} + engines: {node: '>= 12.13.0'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.flow@3.5.0: + resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + mdn-data@2.0.4: + resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.53.0: + resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mini-css-extract-plugin@2.9.2: + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-share@0.18.4: + resolution: {integrity: sha512-q18IurEhV5LanZaT0DxVymeMjTV8FEiLuThA4btpx/iIn131V0mkCs1PDMzAMQChCigxy6xRJORXcUH5XJVaxA==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: '>=17.0.2' + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nth-check@1.0.2: + resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.3: + resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.getownpropertydescriptors@2.1.8: + resolution: {integrity: sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==} + engines: {node: '>= 0.8'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-attribute-case-insensitive@5.0.2: + resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-browser-comments@4.0.0: + resolution: {integrity: sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==} + engines: {node: '>=8'} + peerDependencies: + browserslist: '>=4' + postcss: '>=8' + + postcss-calc@8.2.4: + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + + postcss-color-functional-notation@4.2.4: + resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-color-hex-alpha@8.0.4: + resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-color-rebeccapurple@7.1.1: + resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-colormin@5.3.1: + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-convert-values@5.1.3: + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-custom-media@8.0.2: + resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + + postcss-custom-properties@12.1.11: + resolution: {integrity: sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-custom-selectors@6.0.3: + resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + + postcss-dir-pseudo-class@6.0.5: + resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-discard-comments@5.1.2: + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-duplicates@5.1.0: + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-empty@5.1.1: + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-discard-overridden@5.1.0: + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-double-position-gradients@3.1.2: + resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-env-function@4.0.6: + resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-flexbugs-fixes@5.0.2: + resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} + peerDependencies: + postcss: ^8.1.4 + + postcss-focus-visible@6.0.4: + resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@5.0.4: + resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@3.0.5: + resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-image-set-function@4.0.7: + resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-initial@4.0.1: + resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-lab-function@4.2.1: + resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-loader@6.2.1: + resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-logical@5.0.4: + resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-media-minmax@5.0.0: + resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.1.0 + + postcss-merge-longhand@5.1.7: + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-merge-rules@5.1.4: + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-font-values@5.1.0: + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-gradients@5.1.1: + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-params@5.1.4: + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-minify-selectors@5.2.1: + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.1.0: + resolution: {integrity: sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nesting@10.2.0: + resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-normalize-charset@5.1.0: + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-display-values@5.1.0: + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-positions@5.1.1: + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-repeat-style@5.1.1: + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-string@5.1.0: + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-timing-functions@5.1.0: + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-unicode@5.1.1: + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-url@5.1.0: + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize-whitespace@5.1.1: + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-normalize@10.0.1: + resolution: {integrity: sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==} + engines: {node: '>= 12'} + peerDependencies: + browserslist: '>= 4' + postcss: '>= 8' + + postcss-opacity-percentage@1.1.3: + resolution: {integrity: sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-ordered-values@5.1.3: + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-overflow-shorthand@3.0.4: + resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@7.0.5: + resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-preset-env@7.8.3: + resolution: {integrity: sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-pseudo-class-any-link@7.1.6: + resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-reduce-initial@5.1.2: + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-reduce-transforms@5.1.0: + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-selector-not@6.0.1: + resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + + postcss-svgo@5.1.0: + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-unique-selectors@5.1.1: + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + pretty-format@24.9.0: + resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} + engines: {node: '>= 6'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types-extra@1.1.1: + resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} + peerDependencies: + react: '>=0.14.0' + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + purgecss-webpack-plugin@4.1.3: + resolution: {integrity: sha512-1OHS0WE935w66FjaFSlV06ycmn3/A8a6Q+iVUmmCYAujQ1HPdX+psMXUhASEW0uF1PYEpOlhMc5ApigVqYK08g==} + peerDependencies: + webpack: '*' + + purgecss@4.1.3: + resolution: {integrity: sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==} + hasBin: true + + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.13.1: + resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} + engines: {node: '>=0.6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + react-app-polyfill@3.0.0: + resolution: {integrity: sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==} + engines: {node: '>=14'} + + react-app-rewired@2.2.1: + resolution: {integrity: sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==} + hasBin: true + peerDependencies: + react-scripts: '>=2.1.3' + + react-bootstrap@2.10.6: + resolution: {integrity: sha512-fNvKytSp0nHts1WRnRBJeBEt+I9/ZdrnhIjWOucEduRNvFRU1IXjZueDdWnBiqsTSJ7MckQJi9i/hxGolaRq+g==} + peerDependencies: + '@types/react': '>=16.14.8' + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@types/react': + optional: true + + react-dev-utils@12.0.1: + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-error-overlay@6.0.11: + resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet-async@1.3.0: + resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + + react-i18next@11.18.6: + resolution: {integrity: sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-refresh@0.11.0: + resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} + engines: {node: '>=0.10.0'} + + react-router-dom@7.0.2: + resolution: {integrity: sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.0.2: + resolution: {integrity: sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-scripts@5.0.1: + resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} + engines: {node: '>=14.0.0'} + hasBin: true + peerDependencies: + eslint: '*' + react: '>= 16' + typescript: ^3.2.1 || ^4 + peerDependenciesMeta: + typescript: + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reflect.getprototypeof@1.0.8: + resolution: {integrity: sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + + regex-parser@2.3.0: + resolution: {integrity: sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==} + + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-url-loader@4.0.0: + resolution: {integrity: sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==} + engines: {node: '>=8.9'} + peerDependencies: + rework: 1.0.1 + rework-visit: 1.0.0 + peerDependenciesMeta: + rework: + optional: true + rework-visit: + optional: true + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve.exports@1.1.1: + resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup-plugin-terser@7.0.2: + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize.css@13.0.0: + resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==} + + sass-loader@12.6.0: + resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + + sass@1.54.4: + resolution: {integrity: sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==} + engines: {node: '>=12.0.0'} + hasBin: true + + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@2.7.0: + resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} + engines: {node: '>= 8.9.0'} + + schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} + + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + + source-list-map@2.0.1: + resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + + source-map-explorer@2.5.3: + resolution: {integrity: sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==} + engines: {node: '>=12'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-loader@3.0.2: + resolution: {integrity: sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.20: + resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + static-eval@2.0.2: + resolution: {integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-length@5.0.1: + resolution: {integrity: sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==} + engines: {node: '>=12.20'} + + string-natural-compare@3.0.1: + resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + style-mod@4.1.2: + resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + + stylehacks@5.1.1: + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + svgo@1.3.2: + resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} + engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. + hasBin: true + + svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + + swr@1.3.0: + resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tailwindcss@3.4.16: + resolution: {integrity: sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + + terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + + terser-webpack-plugin@5.3.10: + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.37.0: + resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + throat@6.0.2: + resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + tryer@1.0.1: + resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.3: + resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + + uncontrollable@8.0.4: + resolution: {integrity: sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==} + peerDependencies: + react: '>=16.14.0' + + underscore@1.12.1: + resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unquote@1.1.1: + resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util.promisify@1.0.1: + resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@8.1.1: + resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} + engines: {node: '>=10.12.0'} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} + + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + webpack-dev-server@4.15.2: + resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-manifest-plugin@4.1.1: + resolution: {integrity: sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==} + engines: {node: '>=12.22.0'} + peerDependencies: + webpack: ^4.44.2 || ^5.47.0 + + webpack-sources@1.4.3: + resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} + + webpack-sources@2.3.1: + resolution: {integrity: sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==} + engines: {node: '>=10.13.0'} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack@5.97.1: + resolution: {integrity: sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-boxed-primitive@1.1.0: + resolution: {integrity: sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.0: + resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.16: + resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + workbox-background-sync@6.6.0: + resolution: {integrity: sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==} + + workbox-broadcast-update@6.6.0: + resolution: {integrity: sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==} + + workbox-build@6.6.0: + resolution: {integrity: sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==} + engines: {node: '>=10.0.0'} + + workbox-cacheable-response@6.6.0: + resolution: {integrity: sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==} + deprecated: workbox-background-sync@6.6.0 + + workbox-core@6.6.0: + resolution: {integrity: sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==} + + workbox-expiration@6.6.0: + resolution: {integrity: sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==} + + workbox-google-analytics@6.6.0: + resolution: {integrity: sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained + + workbox-navigation-preload@6.6.0: + resolution: {integrity: sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==} + + workbox-precaching@6.6.0: + resolution: {integrity: sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==} + + workbox-range-requests@6.6.0: + resolution: {integrity: sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==} + + workbox-recipes@6.6.0: + resolution: {integrity: sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==} + + workbox-routing@6.6.0: + resolution: {integrity: sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==} + + workbox-strategies@6.6.0: + resolution: {integrity: sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==} + + workbox-streams@6.6.0: + resolution: {integrity: sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==} + + workbox-sw@6.6.0: + resolution: {integrity: sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==} + + workbox-webpack-plugin@6.6.0: + resolution: {integrity: sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==} + engines: {node: '>=10.0.0'} + peerDependencies: + webpack: ^4.4.0 || ^5.9.0 + + workbox-window@6.6.0: + resolution: {integrity: sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-loader@0.8.1: + resolution: {integrity: sha512-BCEndnUoi3BaZmePkwGGe93txRxLgMhBa/gE725v1/GHnura8QvNs7c4+4C1yyhhKoj3Dg63M7IqhA++15j6ww==} + engines: {node: '>= 14'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zustand@5.0.2: + resolution: {integrity: sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)': + dependencies: + ajv: 8.17.1 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.3': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + convert-source-map: 2.0.0 + debug: 4.4.0 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/eslint-parser@7.25.9(@babel/core@7.26.0)(eslint@8.57.1)': + dependencies: + '@babel/core': 7.26.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + + '@babel/generator@7.26.3': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/traverse': 7.26.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + debug: 4.4.0 + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-member-expression-to-functions@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.25.9': + dependencies: + '@babel/types': 7.26.3 + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-wrap-function': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-member-expression-to-functions': 7.25.9 + '@babel/helper-optimise-call-expression': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.25.9': + dependencies: + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helper-wrap-function@7.25.9': + dependencies: + '@babel/template': 7.25.9 + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + + '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.26.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/template': 7.25.9 + + '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-exponentiation-operator@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-flow-strip-types@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.0) + + '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.26.4 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + + '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-constant-elements@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-display-name@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-development@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.3 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + regenerator-transform: 0.15.2 + + '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-runtime@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-typescript@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-skip-transparent-expression-wrappers': 7.25.9 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.26.0) + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/preset-env@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.26.0 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0) + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-exponentiation-operator': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0) + babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0) + babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0) + babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/types': 7.26.3 + esutils: 2.0.3 + + '@babel/preset-react@7.26.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-development': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-pure-annotations': 7.25.9(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/helper-validator-option': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-modules-commonjs': 7.26.3(@babel/core@7.26.0) + '@babel/plugin-transform-typescript': 7.26.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@babel/traverse@7.26.4': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.3 + '@babel/parser': 7.26.3 + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bcoe/v8-coverage@0.2.3': {} + + '@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)': + dependencies: + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + + '@codemirror/commands@6.7.1': + dependencies: + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + + '@codemirror/lang-angular@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.9 + '@codemirror/lang-javascript': 6.2.2 + '@codemirror/language': 6.10.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@codemirror/lang-cpp@6.0.2': + dependencies: + '@codemirror/language': 6.10.6 + '@lezer/cpp': 1.1.2 + + '@codemirror/lang-css@6.3.1(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/css': 1.1.9 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-go@6.0.1(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/go': 1.0.0 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-html@6.4.9': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.3) + '@codemirror/lang-javascript': 6.2.2 + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/css': 1.1.9 + '@lezer/html': 1.3.10 + + '@codemirror/lang-java@6.0.1': + dependencies: + '@codemirror/language': 6.10.6 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.2': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/lint': 6.8.4 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/javascript': 1.4.21 + + '@codemirror/lang-json@6.0.1': + dependencies: + '@codemirror/language': 6.10.6 + '@lezer/json': 1.0.2 + + '@codemirror/lang-less@6.0.2(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.3) + '@codemirror/language': 6.10.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-liquid@6.2.2': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/lang-html': 6.4.9 + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@codemirror/lang-markdown@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/lang-html': 6.4.9 + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/markdown': 1.3.2 + + '@codemirror/lang-php@6.0.1': + dependencies: + '@codemirror/lang-html': 6.4.9 + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/php': 1.0.2 + + '@codemirror/lang-python@6.1.6(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/python': 1.1.15 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-rust@6.0.1': + dependencies: + '@codemirror/language': 6.10.6 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/sass': 1.0.7 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-sql@6.8.0(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.9 + '@codemirror/lang-javascript': 6.2.2 + '@codemirror/language': 6.10.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.10.6 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/xml': 1.0.5 + + '@codemirror/lang-yaml@6.1.2(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/language': 6.10.6 + '@codemirror/state': 6.5.0 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + '@lezer/yaml': 1.0.3 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/language-data@6.5.1(@codemirror/view@6.35.3)': + dependencies: + '@codemirror/lang-angular': 0.1.3 + '@codemirror/lang-cpp': 6.0.2 + '@codemirror/lang-css': 6.3.1(@codemirror/view@6.35.3) + '@codemirror/lang-go': 6.0.1(@codemirror/view@6.35.3) + '@codemirror/lang-html': 6.4.9 + '@codemirror/lang-java': 6.0.1 + '@codemirror/lang-javascript': 6.2.2 + '@codemirror/lang-json': 6.0.1 + '@codemirror/lang-less': 6.0.2(@codemirror/view@6.35.3) + '@codemirror/lang-liquid': 6.2.2 + '@codemirror/lang-markdown': 6.3.1 + '@codemirror/lang-php': 6.0.1 + '@codemirror/lang-python': 6.1.6(@codemirror/view@6.35.3) + '@codemirror/lang-rust': 6.0.1 + '@codemirror/lang-sass': 6.0.2(@codemirror/view@6.35.3) + '@codemirror/lang-sql': 6.8.0(@codemirror/view@6.35.3) + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.2(@codemirror/view@6.35.3) + '@codemirror/language': 6.10.6 + '@codemirror/legacy-modes': 6.4.2 + transitivePeerDependencies: + - '@codemirror/view' + + '@codemirror/language@6.10.6': + dependencies: + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + style-mod: 4.1.2 + + '@codemirror/legacy-modes@6.4.2': + dependencies: + '@codemirror/language': 6.10.6 + + '@codemirror/lint@6.8.4': + dependencies: + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + crelt: 1.0.6 + + '@codemirror/search@6.5.8': + dependencies: + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 + crelt: 1.0.6 + + '@codemirror/state@6.5.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.35.3': + dependencies: + '@codemirror/state': 6.5.0 + style-mod: 4.1.2 + w3c-keyname: 2.2.8 + + '@commitlint/cli@17.8.1': + dependencies: + '@commitlint/format': 17.8.1 + '@commitlint/lint': 17.8.1 + '@commitlint/load': 17.8.1 + '@commitlint/read': 17.8.1 + '@commitlint/types': 17.8.1 + execa: 5.1.1 + lodash.isfunction: 3.0.9 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + + '@commitlint/config-conventional@17.8.1': + dependencies: + conventional-changelog-conventionalcommits: 6.1.0 + + '@commitlint/config-validator@17.8.1': + dependencies: + '@commitlint/types': 17.8.1 + ajv: 8.17.1 + + '@commitlint/ensure@17.8.1': + dependencies: + '@commitlint/types': 17.8.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@17.8.1': {} + + '@commitlint/format@17.8.1': + dependencies: + '@commitlint/types': 17.8.1 + chalk: 4.1.2 + + '@commitlint/is-ignored@17.8.1': + dependencies: + '@commitlint/types': 17.8.1 + semver: 7.5.4 + + '@commitlint/lint@17.8.1': + dependencies: + '@commitlint/is-ignored': 17.8.1 + '@commitlint/parse': 17.8.1 + '@commitlint/rules': 17.8.1 + '@commitlint/types': 17.8.1 + + '@commitlint/load@17.8.1': + dependencies: + '@commitlint/config-validator': 17.8.1 + '@commitlint/execute-rule': 17.8.1 + '@commitlint/resolve-extends': 17.8.1 + '@commitlint/types': 17.8.1 + '@types/node': 20.5.1 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@4.9.5) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@4.9.5))(typescript@4.9.5) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + resolve-from: 5.0.0 + ts-node: 10.9.2(@types/node@20.5.1)(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + + '@commitlint/message@17.8.1': {} + + '@commitlint/parse@17.8.1': + dependencies: + '@commitlint/types': 17.8.1 + conventional-changelog-angular: 6.0.0 + conventional-commits-parser: 4.0.0 + + '@commitlint/read@17.8.1': + dependencies: + '@commitlint/top-level': 17.8.1 + '@commitlint/types': 17.8.1 + fs-extra: 11.2.0 + git-raw-commits: 2.0.11 + minimist: 1.2.8 + + '@commitlint/resolve-extends@17.8.1': + dependencies: + '@commitlint/config-validator': 17.8.1 + '@commitlint/types': 17.8.1 + import-fresh: 3.3.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + + '@commitlint/rules@17.8.1': + dependencies: + '@commitlint/ensure': 17.8.1 + '@commitlint/message': 17.8.1 + '@commitlint/to-lines': 17.8.1 + '@commitlint/types': 17.8.1 + execa: 5.1.1 + + '@commitlint/to-lines@17.8.1': {} + + '@commitlint/top-level@17.8.1': + dependencies: + find-up: 5.0.0 + + '@commitlint/types@17.8.1': + dependencies: + chalk: 4.1.2 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@csstools/normalize.css@12.1.1': {} + + '@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.49)': + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-color-function@1.1.1(postcss@8.4.49)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-hwb-function@1.0.2(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-ic-unit@1.0.1(postcss@8.4.49)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.4.49)': + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-nested-calc@1.0.0(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@1.1.1(postcss@8.4.49)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-unset-value@1.0.2(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + + '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@fullhuman/postcss-purgecss@4.1.3(postcss@8.4.49)': + dependencies: + postcss: 8.4.49 + purgecss: 4.1.3 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@27.5.1': + dependencies: + '@jest/types': 27.5.1 + '@types/node': 16.18.121 + chalk: 4.1.2 + jest-message-util: 27.5.1 + jest-util: 27.5.1 + slash: 3.0.0 + + '@jest/console@28.1.3': + dependencies: + '@jest/types': 28.1.3 + '@types/node': 16.18.121 + chalk: 4.1.2 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + + '@jest/core@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))': + dependencies: + '@jest/console': 27.5.1 + '@jest/reporters': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 16.18.121 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.8.1 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 27.5.1 + jest-config: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) + jest-haste-map: 27.5.1 + jest-message-util: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-resolve-dependencies: 27.5.1 + jest-runner: 27.5.1 + jest-runtime: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + jest-validate: 27.5.1 + jest-watcher: 27.5.1 + micromatch: 4.0.8 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + + '@jest/environment@27.5.1': + dependencies: + '@jest/fake-timers': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 16.18.121 + jest-mock: 27.5.1 + + '@jest/fake-timers@27.5.1': + dependencies: + '@jest/types': 27.5.1 + '@sinonjs/fake-timers': 8.1.0 + '@types/node': 16.18.121 + jest-message-util: 27.5.1 + jest-mock: 27.5.1 + jest-util: 27.5.1 + + '@jest/globals@27.5.1': + dependencies: + '@jest/environment': 27.5.1 + '@jest/types': 27.5.1 + expect: 27.5.1 + + '@jest/reporters@27.5.1': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 16.18.121 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-haste-map: 27.5.1 + jest-resolve: 27.5.1 + jest-util: 27.5.1 + jest-worker: 27.5.1 + slash: 3.0.0 + source-map: 0.6.1 + string-length: 4.0.2 + terminal-link: 2.1.1 + v8-to-istanbul: 8.1.1 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@28.1.3': + dependencies: + '@sinclair/typebox': 0.24.51 + + '@jest/source-map@27.5.1': + dependencies: + callsites: 3.1.0 + graceful-fs: 4.2.11 + source-map: 0.6.1 + + '@jest/test-result@27.5.1': + dependencies: + '@jest/console': 27.5.1 + '@jest/types': 27.5.1 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-result@28.1.3': + dependencies: + '@jest/console': 28.1.3 + '@jest/types': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@27.5.1': + dependencies: + '@jest/test-result': 27.5.1 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-runtime: 27.5.1 + transitivePeerDependencies: + - supports-color + + '@jest/transform@27.5.1': + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 27.5.1 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-regex-util: 27.5.1 + jest-util: 27.5.1 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + source-map: 0.6.1 + write-file-atomic: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@jest/types@24.9.0': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 1.1.2 + '@types/yargs': 13.0.12 + + '@jest/types@27.5.1': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 16.18.121 + '@types/yargs': 16.0.9 + chalk: 4.1.2 + + '@jest/types@28.1.3': + dependencies: + '@jest/schemas': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 16.18.121 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@lezer/common@1.2.3': {} + + '@lezer/cpp@1.1.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/css@1.1.9': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/go@1.0.0': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/highlight@1.2.1': + dependencies: + '@lezer/common': 1.2.3 + + '@lezer/html@1.3.10': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/javascript@1.4.21': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/json@1.0.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/lr@1.4.2': + dependencies: + '@lezer/common': 1.2.3 + + '@lezer/markdown@1.3.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + + '@lezer/php@1.0.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/python@1.1.15': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/sass@1.0.7': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/xml@1.0.5': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@lezer/yaml@1.0.3': + dependencies: + '@lezer/common': 1.2.3 + '@lezer/highlight': 1.2.1 + '@lezer/lr': 1.4.2 + + '@marijn/find-cluster-break@1.0.2': {} + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + dependencies: + eslint-scope: 5.1.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.1.1': {} + + '@pmmmwh/react-refresh-webpack-plugin@0.5.15(react-refresh@0.11.0)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.97.1))(webpack@5.97.1)': + dependencies: + ansi-html: 0.0.9 + core-js-pure: 3.39.0 + error-stack-parser: 2.1.4 + html-entities: 2.5.2 + loader-utils: 2.0.4 + react-refresh: 0.11.0 + schema-utils: 4.2.0 + source-map: 0.7.4 + webpack: 5.97.1 + optionalDependencies: + type-fest: 1.4.0 + webpack-dev-server: 4.15.2(webpack@5.97.1) + + '@popperjs/core@2.11.8': {} + + '@react-aria/ssr@3.9.7(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.15 + react: 18.3.1 + + '@restart/hooks@0.4.16(react@18.3.1)': + dependencies: + dequal: 2.0.3 + react: 18.3.1 + + '@restart/hooks@0.5.0(react@18.3.1)': + dependencies: + dequal: 2.0.3 + react: 18.3.1 + + '@restart/ui@1.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.9.7(react@18.3.1) + '@restart/hooks': 0.5.0(react@18.3.1) + '@types/warning': 3.0.3 + dequal: 2.0.3 + dom-helpers: 5.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + uncontrollable: 8.0.4(react@18.3.1) + warning: 4.0.3 + + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.2)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 + optionalDependencies: + '@types/babel__core': 7.20.5 + transitivePeerDependencies: + - supports-color + + '@rollup/plugin-node-resolve@11.2.1(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + rollup: 2.79.2 + + '@rollup/plugin-replace@2.4.2(rollup@2.79.2)': + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + magic-string: 0.25.9 + rollup: 2.79.2 + + '@rollup/pluginutils@3.1.0(rollup@2.79.2)': + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.2 + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.10.4': {} + + '@sinclair/typebox@0.24.51': {} + + '@sinonjs/commons@1.8.6': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@8.1.0': + dependencies: + '@sinonjs/commons': 1.8.6 + + '@surma/rollup-plugin-off-main-thread@2.2.3': + dependencies: + ejs: 3.1.10 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.11 + + '@svgr/babel-plugin-add-jsx-attribute@5.4.0': {} + + '@svgr/babel-plugin-remove-jsx-attribute@5.4.0': {} + + '@svgr/babel-plugin-remove-jsx-empty-expression@5.0.1': {} + + '@svgr/babel-plugin-replace-jsx-attribute-value@5.0.1': {} + + '@svgr/babel-plugin-svg-dynamic-title@5.4.0': {} + + '@svgr/babel-plugin-svg-em-dimensions@5.4.0': {} + + '@svgr/babel-plugin-transform-react-native-svg@5.4.0': {} + + '@svgr/babel-plugin-transform-svg-component@5.5.0': {} + + '@svgr/babel-preset@5.5.0': + dependencies: + '@svgr/babel-plugin-add-jsx-attribute': 5.4.0 + '@svgr/babel-plugin-remove-jsx-attribute': 5.4.0 + '@svgr/babel-plugin-remove-jsx-empty-expression': 5.0.1 + '@svgr/babel-plugin-replace-jsx-attribute-value': 5.0.1 + '@svgr/babel-plugin-svg-dynamic-title': 5.4.0 + '@svgr/babel-plugin-svg-em-dimensions': 5.4.0 + '@svgr/babel-plugin-transform-react-native-svg': 5.4.0 + '@svgr/babel-plugin-transform-svg-component': 5.5.0 + + '@svgr/core@5.5.0': + dependencies: + '@svgr/plugin-jsx': 5.5.0 + camelcase: 6.3.0 + cosmiconfig: 7.1.0 + transitivePeerDependencies: + - supports-color + + '@svgr/hast-util-to-babel-ast@5.5.0': + dependencies: + '@babel/types': 7.26.3 + + '@svgr/plugin-jsx@5.5.0': + dependencies: + '@babel/core': 7.26.0 + '@svgr/babel-preset': 5.5.0 + '@svgr/hast-util-to-babel-ast': 5.5.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@svgr/plugin-svgo@5.5.0': + dependencies: + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + svgo: 1.3.2 + + '@svgr/webpack@5.5.0': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-constant-elements': 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/preset-react': 7.26.3(@babel/core@7.26.0) + '@svgr/core': 5.5.0 + '@svgr/plugin-jsx': 5.5.0 + '@svgr/plugin-svgo': 5.5.0 + loader-utils: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@4.2.4': + dependencies: + '@babel/runtime': 7.26.0 + chalk: 2.4.2 + css: 2.2.4 + css.escape: 1.5.1 + jest-diff: 24.9.0 + jest-matcher-utils: 24.9.0 + lodash: 4.17.21 + pretty-format: 24.9.0 + redent: 3.0.0 + + '@testing-library/react@13.4.0(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 8.20.1 + '@types/react-dom': 18.3.5(@types/react@18.3.16) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@testing-library/user-event@13.5.0(@testing-library/dom@8.20.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 8.20.1 + + '@tootallnate/once@1.1.2': {} + + '@trysound/sax@0.2.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.3 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.3 + '@babel/types': 7.26.3 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.3 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 16.18.121 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 16.18.121 + + '@types/color-convert@2.0.4': + dependencies: + '@types/color-name': 1.1.5 + + '@types/color-name@1.1.5': {} + + '@types/color@3.0.6': + dependencies: + '@types/color-convert': 2.0.4 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.0.2 + '@types/node': 16.18.121 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 16.18.121 + + '@types/cookie@0.6.0': {} + + '@types/dompurify@2.4.0': + dependencies: + '@types/trusted-types': 2.0.7 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.6 + + '@types/eslint@8.56.12': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@0.0.39': {} + + '@types/estree@1.0.6': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 16.18.121 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express-serve-static-core@5.0.2': + dependencies: + '@types/node': 16.18.121 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 16.18.121 + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-errors@2.0.4': {} + + '@types/http-proxy@1.17.15': + dependencies: + '@types/node': 16.18.121 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@1.1.2': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-lib-report': 3.0.3 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@27.5.2': + dependencies: + jest-matcher-utils: 27.5.1 + pretty-format: 27.5.1 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/lodash@4.17.13': {} + + '@types/marked@4.3.2': {} + + '@types/mime@1.3.5': {} + + '@types/minimist@1.2.5': {} + + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 16.18.121 + + '@types/node@16.18.121': {} + + '@types/node@20.5.1': {} + + '@types/normalize-package-data@2.4.4': {} + + '@types/parse-json@4.0.2': {} + + '@types/prettier@2.7.3': {} + + '@types/prop-types@15.7.14': {} + + '@types/q@1.5.8': {} + + '@types/qs@6.9.17': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.5(@types/react@18.3.16)': + dependencies: + '@types/react': 18.3.16 + + '@types/react-transition-group@4.4.11': + dependencies: + '@types/react': 18.3.16 + + '@types/react@18.3.16': + dependencies: + '@types/prop-types': 15.7.14 + csstype: 3.1.3 + + '@types/resolve@1.17.1': + dependencies: + '@types/node': 16.18.121 + + '@types/retry@0.12.0': {} + + '@types/semver@7.5.8': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 16.18.121 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.21 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 16.18.121 + '@types/send': 0.17.4 + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 16.18.121 + + '@types/stack-utils@2.0.3': {} + + '@types/trusted-types@2.0.7': {} + + '@types/warning@3.0.3': {} + + '@types/ws@8.5.13': + dependencies: + '@types/node': 16.18.121 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@13.0.12': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yargs@16.0.9': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + debug: 4.4.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.6.3 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/experimental-utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + debug: 4.4.0 + eslint: 8.57.1 + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + eslint: 8.57.1 + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + debug: 4.4.0 + eslint: 8.57.1 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + debug: 4.4.0 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@4.9.5)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.0 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.6.3 + tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@6.21.0(typescript@4.9.5)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.0 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.3 + ts-api-utils: 1.4.3(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@4.9.5)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@4.9.5) + eslint: 8.57.1 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.1': {} + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abab@2.0.6: {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-globals@6.0.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-walk@7.2.0: {} + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@7.4.1: {} + + acorn@8.14.0: {} + + address@1.2.2: {} + + adjust-sourcemap-loader@4.0.0: + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.3.0 + + agent-base@6.0.2: + dependencies: + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-html-community@0.0.8: {} + + ansi-html@0.0.9: {} + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.8 + is-array-buffer: 3.0.4 + + array-flatten@1.1.1: {} + + array-ify@1.0.0: {} + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.5 + is-string: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 + + array.prototype.flat@1.3.2: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + array.prototype.flatmap@1.3.2: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-shim-unscopables: 1.0.2 + + array.prototype.reduce@1.0.7: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-array-method-boxes-properly: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + is-string: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + + arrify@1.0.1: {} + + asap@2.0.6: {} + + ast-types-flow@0.0.8: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + atob@2.1.2: {} + + autoprefixer@10.4.20(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001687 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axe-core@4.10.2: {} + + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@4.1.0: {} + + babel-jest@27.5.1(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 27.5.1(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-loader@8.4.1(@babel/core@7.26.0)(webpack@5.97.1): + dependencies: + '@babel/core': 7.26.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.97.1 + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@27.5.1: + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.3 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.26.0 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + + babel-plugin-named-asset-import@0.3.8(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + + babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0): + dependencies: + '@babel/compat-data': 7.26.3 + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + core-js-compat: 3.39.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-react-remove-prop-types@0.4.24: {} + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@27.5.1(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 27.5.1 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + + babel-preset-react-app@10.0.1: + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.26.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.26.0) + '@babel/plugin-transform-flow-strip-types': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-display-name': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-runtime': 7.25.9(@babel/core@7.26.0) + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/preset-react': 7.26.3(@babel/core@7.26.0) + '@babel/preset-typescript': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.26.0 + babel-plugin-macros: 3.1.0 + babel-plugin-transform-react-remove-prop-types: 0.4.24 + transitivePeerDependencies: + - supports-color + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + batch@0.6.1: {} + + bfj@7.1.0: + dependencies: + bluebird: 3.7.2 + check-types: 11.2.3 + hoopy: 0.1.4 + jsonpath: 1.1.1 + tryer: 1.0.1 + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bluebird@3.7.2: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + bootstrap-icons@1.11.3: {} + + bootstrap@5.3.3(@popperjs/core@2.11.8): + dependencies: + '@popperjs/core': 2.11.8 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-process-hrtime@1.0.0: {} + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001687 + electron-to-chromium: 1.5.72 + node-releases: 2.0.19 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + btoa@1.2.1: {} + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@3.3.0: {} + + builtins@5.1.0: + dependencies: + semver: 7.6.3 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-define-property: 1.0.1 + get-intrinsic: 1.2.5 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase-css@2.0.1: {} + + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001687 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001687: {} + + case-sensitive-paths-webpack-plugin@2.4.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + char-regex@1.0.2: {} + + char-regex@2.0.2: {} + + check-types@11.2.3: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.1: {} + + classnames@2.5.1: {} + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} - /customize-cra/1.0.0: - resolution: {integrity: sha512-DbtaLuy59224U+xCiukkxSq8clq++MOtJ1Et7LED1fLszWe88EoblEYFBJ895sB1mC6B4uu3xPT/IjClELhMbA==} + coa@2.0.2: dependencies: - lodash.flow: 3.5.0 - dev: true + '@types/q': 1.5.8 + chalk: 2.4.2 + q: 1.5.1 - /cz-conventional-changelog/3.3.0: - resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} - engines: {node: '>= 10'} + codemirror@6.0.1(@lezer/common@1.2.3): dependencies: - chalk: 2.4.2 - commitizen: 4.2.5 - conventional-commit-types: 3.0.0 - lodash.map: 4.6.0 - longest: 2.0.1 - word-wrap: 1.2.3 - optionalDependencies: - '@commitlint/load': 17.1.2 + '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3) + '@codemirror/commands': 6.7.1 + '@codemirror/language': 6.10.6 + '@codemirror/lint': 6.8.4 + '@codemirror/search': 6.5.8 + '@codemirror/state': 6.5.0 + '@codemirror/view': 6.35.3 transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - dev: true + - '@lezer/common' - /d3-array/1.2.4: - resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} - dev: false + collect-v8-coverage@1.0.2: {} - /d3-array/3.2.0: - resolution: {integrity: sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==} - engines: {node: '>=12'} + color-convert@1.9.3: dependencies: - internmap: 2.0.3 - dev: false + color-name: 1.1.3 - /d3-axis/1.0.12: - resolution: {integrity: sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==} - dev: false + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 - /d3-axis/3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - dev: false + color-name@1.1.3: {} - /d3-brush/1.1.6: - resolution: {integrity: sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==} - dependencies: - d3-dispatch: 1.0.6 - d3-drag: 1.2.5 - d3-interpolate: 1.4.0 - d3-selection: 1.4.2 - d3-transition: 1.3.2 - dev: false + color-name@1.1.4: {} - /d3-brush/3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} + color-string@1.9.1: dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1_d3-selection@3.0.0 - dev: false + color-name: 1.1.4 + simple-swizzle: 0.2.2 - /d3-chord/1.0.6: - resolution: {integrity: sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==} + color@4.2.3: dependencies: - d3-array: 1.2.4 - d3-path: 1.0.9 - dev: false + color-convert: 2.0.1 + color-string: 1.9.1 - /d3-chord/3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - dependencies: - d3-path: 3.0.1 - dev: false + colord@2.9.3: {} - /d3-collection/1.0.7: - resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} - dev: false + colorette@2.0.20: {} - /d3-color/1.4.1: - resolution: {integrity: sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==} - dev: false + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 - /d3-color/3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - dev: false + commander@13.1.0: {} - /d3-contour/1.3.2: - resolution: {integrity: sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==} - dependencies: - d3-array: 1.2.4 - dev: false + commander@2.20.3: {} - /d3-contour/4.0.0: - resolution: {integrity: sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.0 - dev: false + commander@4.1.1: {} - /d3-delaunay/6.0.2: - resolution: {integrity: sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==} - engines: {node: '>=12'} - dependencies: - delaunator: 5.0.0 - dev: false + commander@7.2.0: {} - /d3-dispatch/1.0.6: - resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} - dev: false + commander@8.3.0: {} - /d3-dispatch/3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - dev: false + common-tags@1.8.2: {} - /d3-drag/1.2.5: - resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==} - dependencies: - d3-dispatch: 1.0.6 - d3-selection: 1.4.2 - dev: false + commondir@1.0.1: {} - /d3-drag/3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} + compare-func@2.0.0: dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - dev: false + array-ify: 1.0.0 + dot-prop: 5.3.0 - /d3-dsv/1.2.0: - resolution: {integrity: sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==} - hasBin: true + compressible@2.0.18: dependencies: - commander: 2.20.3 - iconv-lite: 0.4.24 - rw: 1.3.3 - dev: false + mime-db: 1.53.0 - /d3-dsv/3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true + compression@1.7.5: dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - dev: false + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.0.2 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color - /d3-ease/1.0.7: - resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==} - dev: false + concat-map@0.0.1: {} - /d3-ease/3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - dev: false + confusing-browser-globals@1.0.11: {} + + connect-history-api-fallback@2.0.0: {} - /d3-fetch/1.2.0: - resolution: {integrity: sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==} + content-disposition@0.5.4: dependencies: - d3-dsv: 1.2.0 - dev: false + safe-buffer: 5.2.1 - /d3-fetch/3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} + content-type@1.0.5: {} + + conventional-changelog-angular@6.0.0: dependencies: - d3-dsv: 3.0.1 - dev: false + compare-func: 2.0.0 - /d3-force/1.2.1: - resolution: {integrity: sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==} + conventional-changelog-conventionalcommits@6.1.0: dependencies: - d3-collection: 1.0.7 - d3-dispatch: 1.0.6 - d3-quadtree: 1.0.7 - d3-timer: 1.0.10 - dev: false + compare-func: 2.0.0 - /d3-force/3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} + conventional-commits-parser@4.0.0: dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - dev: false + JSONStream: 1.3.5 + is-text-path: 1.0.1 + meow: 8.1.2 + split2: 3.2.2 - /d3-format/1.4.5: - resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} - dev: false + convert-source-map@1.9.0: {} - /d3-format/3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} - engines: {node: '>=12'} - dev: false + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} - /d3-geo/1.12.1: - resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} + cookie@0.7.1: {} + + cookie@1.0.2: {} + + copy-to-clipboard@3.3.3: dependencies: - d3-array: 1.2.4 - dev: false + toggle-selection: 1.0.6 - /d3-geo/3.0.1: - resolution: {integrity: sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==} - engines: {node: '>=12'} + core-js-compat@3.39.0: dependencies: - d3-array: 3.2.0 - dev: false + browserslist: 4.24.2 - /d3-hierarchy/1.1.9: - resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} - dev: false + core-js-pure@3.39.0: {} - /d3-hierarchy/3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - dev: false + core-js@3.39.0: {} - /d3-interpolate/1.4.0: - resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==} - dependencies: - d3-color: 1.4.1 - dev: false + core-util-is@1.0.3: {} - /d3-interpolate/3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@4.9.5))(ts-node@10.9.2(@types/node@20.5.1)(typescript@4.9.5))(typescript@4.9.5): dependencies: - d3-color: 3.1.0 - dev: false + '@types/node': 20.5.1 + cosmiconfig: 8.3.6(typescript@4.9.5) + ts-node: 10.9.2(@types/node@20.5.1)(typescript@4.9.5) + typescript: 4.9.5 - /d3-path/1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} - dev: false + cosmiconfig@6.0.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 - /d3-path/3.0.1: - resolution: {integrity: sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==} - engines: {node: '>=12'} - dev: false + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 - /d3-polygon/1.0.6: - resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} - dev: false + cosmiconfig@8.3.6(typescript@4.9.5): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 4.9.5 - /d3-polygon/3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} - dev: false + create-require@1.1.1: {} - /d3-quadtree/1.0.7: - resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} - dev: false + crelt@1.0.6: {} - /d3-quadtree/3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - dev: false + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 - /d3-random/1.1.2: - resolution: {integrity: sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==} - dev: false + crypto-random-string@2.0.0: {} - /d3-random/3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} - dev: false + css-blank-pseudo@3.0.3(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /d3-scale-chromatic/1.5.0: - resolution: {integrity: sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==} + css-declaration-sorter@6.4.1(postcss@8.4.49): dependencies: - d3-color: 1.4.1 - d3-interpolate: 1.4.0 - dev: false + postcss: 8.4.49 - /d3-scale-chromatic/3.0.0: - resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==} - engines: {node: '>=12'} + css-has-pseudo@3.0.4(postcss@8.4.49): dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - dev: false + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /d3-scale/2.2.2: - resolution: {integrity: sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==} + css-loader@6.11.0(webpack@5.97.1): dependencies: - d3-array: 1.2.4 - d3-collection: 1.0.7 - d3-format: 1.4.5 - d3-interpolate: 1.4.0 - d3-time: 1.1.0 - d3-time-format: 2.3.0 - dev: false + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.49) + postcss-modules-local-by-default: 4.1.0(postcss@8.4.49) + postcss-modules-scope: 3.2.1(postcss@8.4.49) + postcss-modules-values: 4.0.0(postcss@8.4.49) + postcss-value-parser: 4.2.0 + semver: 7.6.3 + optionalDependencies: + webpack: 5.97.1 - /d3-scale/4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} + css-minimizer-webpack-plugin@3.4.1(webpack@5.97.1): dependencies: - d3-array: 3.2.0 - d3-format: 3.1.0 - d3-interpolate: 3.0.1 - d3-time: 3.0.0 - d3-time-format: 4.1.0 - dev: false + cssnano: 5.1.15(postcss@8.4.49) + jest-worker: 27.5.1 + postcss: 8.4.49 + schema-utils: 4.2.0 + serialize-javascript: 6.0.2 + source-map: 0.6.1 + webpack: 5.97.1 - /d3-selection/1.4.2: - resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==} - dev: false + css-prefers-color-scheme@6.0.3(postcss@8.4.49): + dependencies: + postcss: 8.4.49 - /d3-selection/3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} - dev: false + css-select-base-adapter@0.1.1: {} - /d3-shape/1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + css-select@2.1.0: dependencies: - d3-path: 1.0.9 - dev: false + boolbase: 1.0.0 + css-what: 3.4.2 + domutils: 1.7.0 + nth-check: 1.0.2 - /d3-shape/3.1.0: - resolution: {integrity: sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==} - engines: {node: '>=12'} + css-select@4.3.0: dependencies: - d3-path: 3.0.1 - dev: false + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 - /d3-time-format/2.3.0: - resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} + css-tree@1.0.0-alpha.37: dependencies: - d3-time: 1.1.0 - dev: false + mdn-data: 2.0.4 + source-map: 0.6.1 - /d3-time-format/4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} + css-tree@1.1.3: dependencies: - d3-time: 3.0.0 - dev: false - - /d3-time/1.1.0: - resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} - dev: false + mdn-data: 2.0.14 + source-map: 0.6.1 - /d3-time/3.0.0: - resolution: {integrity: sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==} - engines: {node: '>=12'} - dependencies: - d3-array: 3.2.0 - dev: false + css-what@3.4.2: {} - /d3-timer/1.0.10: - resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} - dev: false + css-what@6.1.0: {} - /d3-timer/3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - dev: false + css.escape@1.5.1: {} - /d3-transition/1.3.2: - resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==} + css@2.2.4: dependencies: - d3-color: 1.4.1 - d3-dispatch: 1.0.6 - d3-ease: 1.0.7 - d3-interpolate: 1.4.0 - d3-selection: 1.4.2 - d3-timer: 1.0.10 - dev: false + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.5.3 + urix: 0.1.0 - /d3-transition/3.0.1_d3-selection@3.0.0: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 + cssdb@7.11.2: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@5.2.14(postcss@8.4.49): + dependencies: + css-declaration-sorter: 6.4.1(postcss@8.4.49) + cssnano-utils: 3.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-calc: 8.2.4(postcss@8.4.49) + postcss-colormin: 5.3.1(postcss@8.4.49) + postcss-convert-values: 5.1.3(postcss@8.4.49) + postcss-discard-comments: 5.1.2(postcss@8.4.49) + postcss-discard-duplicates: 5.1.0(postcss@8.4.49) + postcss-discard-empty: 5.1.1(postcss@8.4.49) + postcss-discard-overridden: 5.1.0(postcss@8.4.49) + postcss-merge-longhand: 5.1.7(postcss@8.4.49) + postcss-merge-rules: 5.1.4(postcss@8.4.49) + postcss-minify-font-values: 5.1.0(postcss@8.4.49) + postcss-minify-gradients: 5.1.1(postcss@8.4.49) + postcss-minify-params: 5.1.4(postcss@8.4.49) + postcss-minify-selectors: 5.2.1(postcss@8.4.49) + postcss-normalize-charset: 5.1.0(postcss@8.4.49) + postcss-normalize-display-values: 5.1.0(postcss@8.4.49) + postcss-normalize-positions: 5.1.1(postcss@8.4.49) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.49) + postcss-normalize-string: 5.1.0(postcss@8.4.49) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.49) + postcss-normalize-unicode: 5.1.1(postcss@8.4.49) + postcss-normalize-url: 5.1.0(postcss@8.4.49) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.49) + postcss-ordered-values: 5.1.3(postcss@8.4.49) + postcss-reduce-initial: 5.1.2(postcss@8.4.49) + postcss-reduce-transforms: 5.1.0(postcss@8.4.49) + postcss-svgo: 5.1.0(postcss@8.4.49) + postcss-unique-selectors: 5.1.1(postcss@8.4.49) + + cssnano-utils@3.1.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + cssnano@5.1.15(postcss@8.4.49): + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.4.49) + lilconfig: 2.1.0 + postcss: 8.4.49 + yaml: 1.10.2 + + csso@4.2.0: dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - dev: false + css-tree: 1.1.3 - /d3-voronoi/1.1.4: - resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} - dev: false + cssom@0.3.8: {} - /d3-zoom/1.8.3: - resolution: {integrity: sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==} - dependencies: - d3-dispatch: 1.0.6 - d3-drag: 1.2.5 - d3-interpolate: 1.4.0 - d3-selection: 1.4.2 - d3-transition: 1.3.2 - dev: false + cssom@0.4.4: {} - /d3-zoom/3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1_d3-selection@3.0.0 - dev: false - - /d3/5.16.0: - resolution: {integrity: sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==} - dependencies: - d3-array: 1.2.4 - d3-axis: 1.0.12 - d3-brush: 1.1.6 - d3-chord: 1.0.6 - d3-collection: 1.0.7 - d3-color: 1.4.1 - d3-contour: 1.3.2 - d3-dispatch: 1.0.6 - d3-drag: 1.2.5 - d3-dsv: 1.2.0 - d3-ease: 1.0.7 - d3-fetch: 1.2.0 - d3-force: 1.2.1 - d3-format: 1.4.5 - d3-geo: 1.12.1 - d3-hierarchy: 1.1.9 - d3-interpolate: 1.4.0 - d3-path: 1.0.9 - d3-polygon: 1.0.6 - d3-quadtree: 1.0.7 - d3-random: 1.1.2 - d3-scale: 2.2.2 - d3-scale-chromatic: 1.5.0 - d3-selection: 1.4.2 - d3-shape: 1.3.7 - d3-time: 1.1.0 - d3-time-format: 2.3.0 - d3-timer: 1.0.10 - d3-transition: 1.3.2 - d3-voronoi: 1.1.4 - d3-zoom: 1.8.3 - dev: false - - /d3/7.6.1: - resolution: {integrity: sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==} - engines: {node: '>=12'} + cssstyle@2.3.0: dependencies: - d3-array: 3.2.0 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.0 - d3-delaunay: 6.0.2 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.0 - d3-geo: 3.0.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.0.1 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.0.0 - d3-selection: 3.0.0 - d3-shape: 3.1.0 - d3-time: 3.0.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1_d3-selection@3.0.0 - d3-zoom: 3.0.0 - dev: false - - /dagre-d3/0.6.4: - resolution: {integrity: sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==} - dependencies: - d3: 5.16.0 - dagre: 0.8.5 - graphlib: 2.1.8 - lodash: 4.17.21 - dev: false + cssom: 0.3.8 - /dagre/0.8.5: - resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==} + csstype@3.1.3: {} + + customize-cra@1.0.0: dependencies: - graphlib: 2.1.8 - lodash: 4.17.21 - dev: false + lodash.flow: 3.5.0 - /damerau-levenshtein/1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + damerau-levenshtein@1.0.8: {} - /dargs/7.0.0: - resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} - engines: {node: '>=8'} - dev: true + dargs@7.0.0: {} - /data-urls/2.0.0: - resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} - engines: {node: '>=10'} + data-urls@2.0.0: dependencies: abab: 2.0.6 whatwg-mimetype: 2.3.0 whatwg-url: 8.7.0 - /dateformat/3.0.3: - resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} - dev: true + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 - /dayjs/1.11.5: - resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==} - dev: false + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 - /debug/2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + dayjs@1.11.13: {} + + debug@2.6.9: dependencies: ms: 2.0.0 - /debug/3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@3.2.7: dependencies: ms: 2.1.3 - /debug/4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.4.0: dependencies: - ms: 2.1.2 + ms: 2.1.3 - /decamelize-keys/1.1.0: - resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==} - engines: {node: '>=0.10.0'} + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 map-obj: 1.0.1 - dev: true - /decamelize/1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: true + decamelize@1.2.0: {} - /decimal.js/10.4.1: - resolution: {integrity: sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==} + decimal.js@10.4.3: {} - /decode-uri-component/0.2.0: - resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} - engines: {node: '>=0.10'} - dev: false + decode-uri-component@0.2.2: {} - /dedent/0.7.0: - resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dedent@0.7.0: {} - /deep-is/0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.5 + is-arguments: 1.1.1 + is-array-buffer: 3.0.4 + is-date-object: 1.0.5 + is-regex: 1.2.0 + is-shared-array-buffer: 1.0.3 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + side-channel: 1.0.6 + which-boxed-primitive: 1.1.0 + which-collection: 1.0.2 + which-typed-array: 1.1.16 - /deepmerge/4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} - engines: {node: '>=0.10.0'} + deep-is@0.1.4: {} - /default-gateway/6.0.3: - resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} - engines: {node: '>= 10'} + deepmerge@4.3.1: {} + + default-gateway@6.0.3: dependencies: execa: 5.1.1 - /defaults/1.0.3: - resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} + define-data-property@1.1.4: dependencies: - clone: 1.0.4 - dev: true + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 - /define-lazy-prop/2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} + define-lazy-prop@2.0.0: {} - /define-properties/1.1.4: - resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} - engines: {node: '>= 0.4'} + define-properties@1.2.1: dependencies: - has-property-descriptors: 1.0.0 + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 object-keys: 1.1.1 - /defined/1.0.0: - resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} - - /delaunator/5.0.0: - resolution: {integrity: sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==} - dependencies: - robust-predicates: 3.0.1 - dev: false - - /delayed-stream/1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - /depd/1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - - /depd/2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + delayed-stream@1.0.0: {} - /dequal/2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: false + depd@1.1.2: {} - /destroy/1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + depd@2.0.0: {} - /detect-file/1.0.0: - resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} - engines: {node: '>=0.10.0'} - dev: true + dequal@2.0.3: {} - /detect-indent/6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - dev: true + destroy@1.2.0: {} - /detect-newline/3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} + detect-newline@3.1.0: {} - /detect-node/2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + detect-node@2.1.0: {} - /detect-port-alt/1.1.6: - resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} - engines: {node: '>= 4.2.1'} - hasBin: true + detect-port-alt@1.1.6: dependencies: - address: 1.2.1 + address: 1.2.2 debug: 2.6.9 transitivePeerDependencies: - supports-color - /detective/5.2.1: - resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} - engines: {node: '>=0.8.0'} - hasBin: true - dependencies: - acorn-node: 1.8.2 - defined: 1.0.0 - minimist: 1.2.6 + didyoumean@1.2.2: {} - /didyoumean/1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@24.9.0: {} - /diff-sequences/24.9.0: - resolution: {integrity: sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==} - engines: {node: '>= 6'} - dev: false + diff-sequences@27.5.1: {} - /diff-sequences/27.5.1: - resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + diff@4.0.2: {} - /diff/4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dev: true + diff@5.2.0: {} - /dir-glob/3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + dijkstrajs@1.0.3: {} + + dir-glob@3.0.1: dependencies: path-type: 4.0.0 - /dlv/1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - /dns-equal/1.0.0: - resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} + dlv@1.1.3: {} - /dns-packet/5.4.0: - resolution: {integrity: sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==} - engines: {node: '>=6'} + dns-packet@5.6.1: dependencies: - '@leichtgewicht/ip-codec': 2.0.4 + '@leichtgewicht/ip-codec': 2.0.5 - /doctrine/2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + doctrine@2.1.0: dependencies: esutils: 2.0.3 - /doctrine/3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + doctrine@3.0.0: dependencies: esutils: 2.0.3 - /dom-accessibility-api/0.5.14: - resolution: {integrity: sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==} - dev: true + dom-accessibility-api@0.5.16: {} - /dom-converter/0.2.0: - resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + dom-converter@0.2.0: dependencies: utila: 0.4.0 - /dom-helpers/5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.19.0 - csstype: 3.1.1 - dev: false + '@babel/runtime': 7.26.0 + csstype: 3.1.3 - /dom-serializer/0.2.2: - resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + dom-serializer@0.2.2: dependencies: domelementtype: 2.3.0 entities: 2.2.0 - /dom-serializer/1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 entities: 2.2.0 - /domelementtype/1.3.1: - resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + domelementtype@1.3.1: {} - /domelementtype/2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@2.3.0: {} - /domexception/2.0.1: - resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} - engines: {node: '>=8'} + domexception@2.0.1: dependencies: webidl-conversions: 5.0.0 - /domhandler/4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} + domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - /dompurify/2.4.0: - resolution: {integrity: sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==} - dev: false - - /domutils/1.7.0: - resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + domutils@1.7.0: dependencies: dom-serializer: 0.2.2 domelementtype: 1.3.1 - /domutils/2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 - /dot-case/3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.4.0 + tslib: 2.8.1 - /dot-prop/5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 - dev: true - /dotenv-expand/5.1.0: - resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + dotenv-expand@5.1.0: {} - /dotenv/10.0.0: - resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} - engines: {node: '>=10'} + dotenv@10.0.0: {} - /duplexer/0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dunder-proto@1.0.0: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 - /eastasianwidth/0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true + duplexer@0.1.2: {} - /ee-first/1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + eastasianwidth@0.2.0: {} - /ejs/3.1.8: - resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} - engines: {node: '>=0.10.0'} - hasBin: true + ee-first@1.1.1: {} + + ejs@3.1.10: dependencies: - jake: 10.8.5 + jake: 10.9.2 + + electron-to-chromium@1.5.72: {} - /electron-to-chromium/1.4.256: - resolution: {integrity: sha512-x+JnqyluoJv8I0U9gVe+Sk2st8vF0CzMt78SXxuoWCooLLY2k5VerIBdpvG7ql6GKI4dzNnPjmqgDJ76EdaAKw==} + emittery@0.10.2: {} - /emittery/0.10.2: - resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} - engines: {node: '>=12'} + emittery@0.8.1: {} - /emittery/0.8.1: - resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} - engines: {node: '>=10'} + emoji-regex@10.4.0: {} - /emoji-regex/8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@8.0.0: {} - /emoji-regex/9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emoji-regex@9.2.2: {} - /emojis-list/3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} + emojis-list@3.0.0: {} - /encodeurl/1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} + encodeurl@1.0.2: {} - /enhanced-resolve/5.10.0: - resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==} - engines: {node: '>=10.13.0'} + encodeurl@2.0.0: {} + + enhanced-resolve@5.17.1: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 - /entities/2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@2.2.0: {} - /env-cmd/10.1.0: - resolution: {integrity: sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - commander: 4.1.1 - cross-spawn: 7.0.3 - dev: true + environment@1.1.0: {} - /error-ex/1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - /error-stack-parser/2.1.4: - resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 - /es-abstract/1.20.2: - resolution: {integrity: sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - es-to-primitive: 1.2.1 - function-bind: 1.1.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.1.3 - get-symbol-description: 1.0.0 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-symbols: 1.0.3 - internal-slot: 1.0.3 - is-callable: 1.2.6 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 + es-abstract@1.23.5: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.5 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.2.0 + is-shared-array-buffer: 1.0.3 + is-string: 1.1.0 + is-typed-array: 1.1.13 is-weakref: 1.0.2 - object-inspect: 1.12.2 + object-inspect: 1.13.3 object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.4.3 - string.prototype.trimend: 1.0.5 - string.prototype.trimstart: 1.0.5 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.3 + typed-array-length: 1.0.7 unbox-primitive: 1.0.2 + which-typed-array: 1.1.16 - /es-array-method-boxes-properly/1.0.0: - resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + es-array-method-boxes-properly@1.0.0: {} - /es-module-lexer/0.9.3: - resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + es-define-property@1.0.1: {} - /es-shim-unscopables/1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: dependencies: - has: 1.0.3 + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + has-symbols: 1.1.0 + is-arguments: 1.1.1 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.0 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 - /es-to-primitive/1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} + es-iterator-helpers@1.2.0: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-set-tostringtag: 2.0.3 + function-bind: 1.1.2 + get-intrinsic: 1.2.5 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.0.7 + iterator.prototype: 1.1.3 + safe-array-concat: 1.1.2 + + es-module-lexer@1.5.4: {} + + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.5 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: dependencies: - is-callable: 1.2.6 + is-callable: 1.2.7 is-date-object: 1.0.5 - is-symbol: 1.0.4 + is-symbol: 1.1.0 - /escalade/3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} + escalade@3.2.0: {} - /escape-html/1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-html@1.0.3: {} - /escape-string-regexp/1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} + escape-string-regexp@1.0.5: {} - /escape-string-regexp/2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} + escape-string-regexp@2.0.0: {} - /escape-string-regexp/4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + escape-string-regexp@4.0.0: {} - /escodegen/2.0.0: - resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} - engines: {node: '>=6.0'} - hasBin: true + escodegen@1.14.3: dependencies: esprima: 4.0.1 - estraverse: 5.3.0 + estraverse: 4.3.0 esutils: 2.0.3 optionator: 0.8.3 optionalDependencies: source-map: 0.6.1 - /eslint-config-airbnb-base/15.0.0_hdzsmr7kawaomymueo2tso6fjq: - resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} - engines: {node: ^10.12.0 || >=12.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.2 - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.23.1 - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - object.assign: 4.1.4 - object.entries: 1.1.5 - semver: 6.3.0 - dev: true - - /eslint-config-airbnb-typescript/17.0.0_j57hrpt2hfp47otngkwtnuyxpa: - resolution: {integrity: sha512-elNiuzD0kPAPTXjFWg+lE24nMdHMtuxgYoD30OyMD6yrW1AhFZPAg27VX7d3tzOErw+dgJTNWfRSDqEcXb4V0g==} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.13.0 - '@typescript-eslint/parser': ^5.0.0 - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.3 + escodegen@2.1.0: dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - eslint: 8.23.1 - eslint-config-airbnb-base: 15.0.0_hdzsmr7kawaomymueo2tso6fjq - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - dev: true + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 - /eslint-config-airbnb/19.0.4_4zstfqq5uopk5xuvotejlnl36y: - resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} - engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.3 - eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-react: ^7.28.0 - eslint-plugin-react-hooks: ^4.3.0 - dependencies: - eslint: 8.23.1 - eslint-config-airbnb-base: 15.0.0_hdzsmr7kawaomymueo2tso6fjq - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-jsx-a11y: 6.6.1_eslint@8.23.1 - eslint-plugin-react: 7.31.8_eslint@8.23.1 - eslint-plugin-react-hooks: 4.6.0_eslint@8.23.1 - object.assign: 4.1.4 - object.entries: 1.1.5 - dev: true - - /eslint-config-prettier/8.5.0_eslint@8.23.1: - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + eslint-compat-utils@0.5.1(eslint@8.57.1): dependencies: - eslint: 8.23.1 - dev: true + eslint: 8.57.1 + semver: 7.6.3 - /eslint-config-react-app/7.0.1_ep5hkfurrjf46kbnkcej3benz4: - resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} - engines: {node: '>=14.0.0'} - peerDependencies: - eslint: ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1): dependencies: - '@babel/core': 7.19.1 - '@babel/eslint-parser': 7.19.1_zdglor7vg7osicr5spasq6cc5a - '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + confusing-browser-globals: 1.0.11 + eslint: 8.57.1 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + object.assign: 4.1.5 + object.entries: 1.1.8 + semver: 6.3.1 + + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1): + dependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.2(eslint@8.57.1))(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.2(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) + object.assign: 4.1.5 + object.entries: 1.1.8 + + eslint-config-prettier@9.1.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)))(typescript@4.9.5): + dependencies: + '@babel/core': 7.26.0 + '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) + '@rushstack/eslint-patch': 1.10.4 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.23.1 - eslint-plugin-flowtype: 8.0.3_yb7llsfoad77a2vx2os7pgkvmi - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-jest: 25.7.0_hpujes4m5fznz335nz2hgbshme - eslint-plugin-jsx-a11y: 6.6.1_eslint@8.23.1 - eslint-plugin-react: 7.31.8_eslint@8.23.1 - eslint-plugin-react-hooks: 4.6.0_eslint@8.23.1 - eslint-plugin-testing-library: 5.6.4_irgkl5vooow2ydyo6aokmferha - typescript: 4.8.3 + eslint: 8.57.1 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)))(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.2(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) + eslint-plugin-testing-library: 5.11.1(eslint@8.57.1)(typescript@4.9.5) + optionalDependencies: + typescript: 4.9.5 transitivePeerDependencies: - '@babel/plugin-syntax-flow' - '@babel/plugin-transform-react-jsx' @@ -5433,421 +10765,324 @@ packages: - jest - supports-color - /eslint-config-standard-with-typescript/22.0.0_fsqc7gnfr7ufpr4slszrtm5abq: - resolution: {integrity: sha512-VA36U7UlFpwULvkdnh6MQj5GAV2Q+tT68ALLAwJP0ZuNXU2m0wX07uxX4qyLRdHgSzH4QJ73CveKBuSOYvh7vQ==} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: ^15.0.0 - eslint-plugin-promise: ^6.0.0 - typescript: '*' + eslint-config-standard-with-typescript@39.1.1(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@4.9.5): dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - eslint: 8.23.1 - eslint-config-standard: 17.0.0_4nulviyjkaspo7v2xlghuwxbf4 - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-n: 15.2.5_eslint@8.23.1 - eslint-plugin-promise: 6.0.1_eslint@8.23.1 - typescript: 4.8.3 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + eslint-plugin-n: 16.6.2(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) + typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: true - /eslint-config-standard/17.0.0_4nulviyjkaspo7v2xlghuwxbf4: - resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} - peerDependencies: - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: ^15.0.0 - eslint-plugin-promise: ^6.0.0 + eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: - eslint: 8.23.1 - eslint-plugin-import: 2.26.0_cxqatnnjiq7ozd2bkspxnuicdq - eslint-plugin-n: 15.2.5_eslint@8.23.1 - eslint-plugin-promise: 6.0.1_eslint@8.23.1 - dev: true + eslint: 8.57.1 + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1) + eslint-plugin-n: 16.6.2(eslint@8.57.1) + eslint-plugin-promise: 6.6.0(eslint@8.57.1) - /eslint-import-resolver-node/0.3.6: - resolution: {integrity: sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==} + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 - resolve: 1.22.1 + is-core-module: 2.15.1 + resolve: 1.22.8 transitivePeerDependencies: - supports-color - /eslint-module-utils/2.7.4_p4kveujfv4nmzmj4mix5hvnxlm: - resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha debug: 3.2.7 - eslint: 8.23.1 - eslint-import-resolver-node: 0.3.6 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - /eslint-plugin-es/4.1.0_eslint@8.23.1: - resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' + eslint-plugin-es-x@7.8.0(eslint@8.57.1): dependencies: - eslint: 8.23.1 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - dev: true + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) - /eslint-plugin-flowtype/8.0.3_yb7llsfoad77a2vx2os7pgkvmi: - resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@babel/plugin-syntax-flow': ^7.14.5 - '@babel/plugin-transform-react-jsx': ^7.14.9 - eslint: ^8.1.0 + eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(eslint@8.57.1): dependencies: - '@babel/plugin-syntax-flow': 7.18.6_@babel+core@7.19.1 - '@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.19.1 - eslint: 8.23.1 + '@babel/plugin-syntax-flow': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + eslint: 8.57.1 lodash: 4.17.21 string-natural-compare: 3.0.1 - /eslint-plugin-import/2.26.0_cxqatnnjiq7ozd2bkspxnuicdq: - resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1): dependencies: - '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - array-includes: 3.1.5 - array.prototype.flat: 1.3.0 - debug: 2.6.9 + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.23.1 - eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.4_p4kveujfv4nmzmj4mix5hvnxlm - has: 1.0.3 - is-core-module: 2.10.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.15.1 is-glob: 4.0.3 minimatch: 3.1.2 - object.values: 1.1.5 - resolve: 1.22.1 - tsconfig-paths: 3.14.1 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - /eslint-plugin-jest/25.7.0_hpujes4m5fznz335nz2hgbshme: - resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.15.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 + semver: 6.3.1 + string.prototype.trimend: 1.0.8 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)))(typescript@4.9.5): dependencies: - '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/experimental-utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - eslint: 8.23.1 - jest: 27.5.1 + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5) + jest: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) transitivePeerDependencies: - supports-color - typescript - /eslint-plugin-jsx-a11y/6.6.1_eslint@8.23.1: - resolution: {integrity: sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): dependencies: - '@babel/runtime': 7.19.0 - aria-query: 4.2.2 - array-includes: 3.1.5 - ast-types-flow: 0.0.7 - axe-core: 4.4.3 - axobject-query: 2.2.0 + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + ast-types-flow: 0.0.8 + axe-core: 4.10.2 + axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.23.1 - has: 1.0.3 - jsx-ast-utils: 3.3.3 - language-tags: 1.0.5 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 minimatch: 3.1.2 - semver: 6.3.0 - - /eslint-plugin-n/15.2.5_eslint@8.23.1: - resolution: {integrity: sha512-8+BYsqiyZfpu6NXmdLOXVUfk8IocpCjpd8nMRRH0A9ulrcemhb2VI9RSJMEy5udx++A/YcVPD11zT8hpFq368g==} - engines: {node: '>=12.22.0'} - peerDependencies: - eslint: '>=7.0.0' - dependencies: - builtins: 5.0.1 - eslint: 8.23.1 - eslint-plugin-es: 4.1.0_eslint@8.23.1 - eslint-utils: 3.0.0_eslint@8.23.1 - ignore: 5.2.0 - is-core-module: 2.10.0 + object.fromentries: 2.0.8 + safe-regex-test: 1.0.3 + string.prototype.includes: 2.0.1 + + eslint-plugin-n@16.6.2(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + builtins: 5.1.0 + eslint: 8.57.1 + eslint-plugin-es-x: 7.8.0(eslint@8.57.1) + get-tsconfig: 4.8.1 + globals: 13.24.0 + ignore: 5.3.2 + is-builtin-module: 3.2.1 + is-core-module: 2.15.1 minimatch: 3.1.2 - resolve: 1.22.1 - semver: 7.3.7 - dev: true + resolve: 1.22.8 + semver: 7.6.3 - /eslint-plugin-prettier/4.2.1_cabrci5exjdaojcvd6xoxgeowu: - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true + eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.2): dependencies: - eslint: 8.23.1 - eslint-config-prettier: 8.5.0_eslint@8.23.1 - prettier: 2.7.1 + eslint: 8.57.1 + prettier: 3.4.2 prettier-linter-helpers: 1.0.0 - dev: true + synckit: 0.9.2 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 9.1.0(eslint@8.57.1) - /eslint-plugin-promise/6.0.1_eslint@8.23.1: - resolution: {integrity: sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint-plugin-promise@6.6.0(eslint@8.57.1): dependencies: - eslint: 8.23.1 - dev: true + eslint: 8.57.1 - /eslint-plugin-react-hooks/4.6.0_eslint@8.23.1: - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: - eslint: 8.23.1 + eslint: 8.57.1 - /eslint-plugin-react/7.31.8_eslint@8.23.1: - resolution: {integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-react@7.37.2(eslint@8.57.1): dependencies: - array-includes: 3.1.5 - array.prototype.flatmap: 1.3.0 + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - eslint: 8.23.1 + es-iterator-helpers: 1.2.0 + eslint: 8.57.1 estraverse: 5.3.0 - jsx-ast-utils: 3.3.3 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 minimatch: 3.1.2 - object.entries: 1.1.5 - object.fromentries: 2.0.5 - object.hasown: 1.1.1 - object.values: 1.1.5 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.values: 1.2.0 prop-types: 15.8.1 - resolve: 2.0.0-next.4 - semver: 6.3.0 - string.prototype.matchall: 4.0.7 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + string.prototype.repeat: 1.0.0 - /eslint-plugin-testing-library/5.6.4_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-0oW3tC5NNT2WexmJ3848a/utawOymw4ibl3/NkwywndVAz2hT9+ab70imA7ccg3RaScQgMvJT60OL00hpmJvrg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 + eslint-plugin-testing-library@5.11.1(eslint@8.57.1)(typescript@4.9.5): dependencies: - '@typescript-eslint/utils': 5.38.0_irgkl5vooow2ydyo6aokmferha - eslint: 8.23.1 + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript - /eslint-scope/5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - /eslint-scope/7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - /eslint-utils/2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - dependencies: - eslint-visitor-keys: 1.3.0 - dev: true - - /eslint-utils/3.0.0_eslint@8.23.1: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.23.1 - eslint-visitor-keys: 2.1.0 - - /eslint-visitor-keys/1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: true - - /eslint-visitor-keys/2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} + eslint-visitor-keys@2.1.0: {} - /eslint-visitor-keys/3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@3.4.3: {} - /eslint-webpack-plugin/3.2.0_cnsurwdbw57xgwxuf5k544xt5e: - resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} - engines: {node: '>= 12.13.0'} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - webpack: ^5.0.0 + eslint-webpack-plugin@3.2.0(eslint@8.57.1)(webpack@5.97.1): dependencies: - '@types/eslint': 8.4.6 - eslint: 8.23.1 + '@types/eslint': 8.56.12 + eslint: 8.57.1 jest-worker: 28.1.3 - micromatch: 4.0.5 + micromatch: 4.0.8 normalize-path: 3.0.0 - schema-utils: 4.0.0 - webpack: 5.74.0 + schema-utils: 4.2.0 + webpack: 5.97.1 - /eslint/8.23.1: - resolution: {integrity: sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint@8.57.1: dependencies: - '@eslint/eslintrc': 1.3.2 - '@humanwhocodes/config-array': 0.10.4 - '@humanwhocodes/gitignore-to-minimatch': 1.0.2 + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.1 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 + cross-spawn: 7.0.6 + debug: 4.4.0 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0_eslint@8.23.1 - eslint-visitor-keys: 3.3.0 - espree: 9.4.0 - esquery: 1.4.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.17.0 - globby: 11.1.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.0 - import-fresh: 3.3.0 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - js-sdsl: 4.1.4 + is-path-inside: 3.0.3 js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 + optionator: 0.9.4 strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - /espree/9.4.0: - resolution: {integrity: sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@9.6.1: dependencies: - acorn: 8.8.0 - acorn-jsx: 5.3.2_acorn@8.8.0 - eslint-visitor-keys: 3.3.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 - /esprima/4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true + esprima@1.2.2: {} - /esquery/1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} + esprima@4.0.1: {} + + esquery@1.6.0: dependencies: estraverse: 5.3.0 - /esrecurse/4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - /estraverse/4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} + estraverse@4.3.0: {} - /estraverse/5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} + estraverse@5.3.0: {} - /estree-walker/1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + estree-walker@1.0.1: {} - /esutils/2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + esutils@2.0.3: {} - /etag/1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + etag@1.8.1: {} - /eventemitter3/4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@4.0.7: {} - /events/3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} + eventemitter3@5.0.1: {} - /execa/5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + events@3.3.0: {} + + execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -5857,71 +11092,55 @@ packages: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - /execa/6.1.0: - resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + execa@8.0.1: dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 3.0.1 + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 is-stream: 3.0.0 merge-stream: 2.0.0 - npm-run-path: 5.1.0 + npm-run-path: 5.3.0 onetime: 6.0.0 - signal-exit: 3.0.7 + signal-exit: 4.1.0 strip-final-newline: 3.0.0 - dev: true - - /exit/0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - /expand-tilde/2.0.2: - resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} - engines: {node: '>=0.10.0'} - dependencies: - homedir-polyfill: 1.0.3 - dev: true + exit@0.1.2: {} - /expect/27.5.1: - resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + expect@27.5.1: dependencies: '@jest/types': 27.5.1 jest-get-type: 27.5.1 jest-matcher-utils: 27.5.1 jest-message-util: 27.5.1 - /express/4.18.1: - resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==} - engines: {node: '>= 0.10.0'} + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.0 + body-parser: 1.20.3 content-disposition: 0.5.4 - content-type: 1.0.4 - cookie: 0.5.0 + content-type: 1.0.5 + cookie: 0.7.1 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.2.0 + finalhandler: 1.3.1 fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 1.0.1 + merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.10.3 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 + send: 0.19.0 + serve-static: 1.16.2 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 1.6.18 @@ -5930,98 +11149,60 @@ packages: transitivePeerDependencies: - supports-color - /external-editor/3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true - - /fast-deep-equal/3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-deep-equal@3.1.3: {} - /fast-diff/1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} - dev: true + fast-diff@1.3.0: {} - /fast-glob/3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.8 - /fast-json-stable-stringify/2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stable-stringify@2.1.0: {} - /fast-levenshtein/2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-levenshtein@2.0.6: {} - /fastq/1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + fast-uri@3.0.3: {} + + fastq@1.17.1: dependencies: reusify: 1.0.4 - - /faye-websocket/0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} + + faye-websocket@0.11.4: dependencies: websocket-driver: 0.7.4 - /fb-watchman/2.0.1: - resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} + fb-watchman@2.0.2: dependencies: bser: 2.1.1 - /figures/3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - - /file-entry-cache/6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@6.0.1: dependencies: - flat-cache: 3.0.4 + flat-cache: 3.2.0 - /file-loader/6.2.0_webpack@5.74.0: - resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 + file-loader@6.2.0(webpack@5.97.1): dependencies: - loader-utils: 2.0.2 - schema-utils: 3.1.1 - webpack: 5.74.0 + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.97.1 - /filelist/1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + filelist@1.0.4: dependencies: - minimatch: 5.1.0 + minimatch: 5.1.6 - /filesize/8.0.7: - resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} - engines: {node: '>= 0.4.0'} + filesize@8.0.7: {} - /fill-range/7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - /finalhandler/1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} + finalhandler@1.3.1: dependencies: debug: 2.6.9 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 @@ -6030,281 +11211,187 @@ packages: transitivePeerDependencies: - supports-color - /find-cache-dir/3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} + find-cache-dir@3.3.2: dependencies: commondir: 1.0.1 make-dir: 3.1.0 pkg-dir: 4.2.0 - /find-node-modules/2.1.3: - resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} - dependencies: - findup-sync: 4.0.0 - merge: 2.1.1 - dev: true - - /find-root/1.1.0: - resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} - dev: true - - /find-up/2.1.0: - resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} - engines: {node: '>=4'} - dependencies: - locate-path: 2.0.0 - dev: true - - /find-up/3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} + find-up@3.0.0: dependencies: locate-path: 3.0.0 - /find-up/4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - /find-up/5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - /findup-sync/4.0.0: - resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} - engines: {node: '>= 8'} - dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.5 - resolve-dir: 1.0.1 - dev: true - - /flat-cache/3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@3.2.0: dependencies: - flatted: 3.2.7 + flatted: 3.3.2 + keyv: 4.5.4 rimraf: 3.0.2 - /flatted/3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + flatted@3.3.2: {} - /follow-redirects/1.15.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true + follow-redirects@1.15.9: {} - /fork-ts-checker-webpack-plugin/6.5.2_npfwkgbcmgrbevrxnqgustqabe: - resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} - engines: {node: '>=10', yarn: '>=1.0.0'} - peerDependencies: - eslint: '>= 6' - typescript: '>= 2.7' - vue-template-compiler: '*' - webpack: '>= 4' - peerDependenciesMeta: - eslint: - optional: true - vue-template-compiler: - optional: true + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@4.9.5)(webpack@5.97.1): dependencies: - '@babel/code-frame': 7.18.6 - '@types/json-schema': 7.0.11 + '@babel/code-frame': 7.26.2 + '@types/json-schema': 7.0.15 chalk: 4.1.2 - chokidar: 3.5.3 + chokidar: 3.6.0 cosmiconfig: 6.0.0 - deepmerge: 4.2.2 - eslint: 8.23.1 + deepmerge: 4.3.1 fs-extra: 9.1.0 glob: 7.2.3 - memfs: 3.4.7 + memfs: 3.5.3 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.7 + semver: 7.6.3 tapable: 1.1.3 - typescript: 4.8.3 - webpack: 5.74.0 + typescript: 4.9.5 + webpack: 5.97.1 + optionalDependencies: + eslint: 8.57.1 - /form-data/3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} + form-data@3.0.2: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - /form-data/4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + form-data@4.0.1: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: false - /forwarded/0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + forwarded@0.2.0: {} - /fraction.js/4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + fraction.js@4.3.7: {} - /fresh/0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + fresh@0.5.2: {} - /fs-extra/10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.1 + + fs-extra@10.1.0: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jsonfile: 6.1.0 - universalify: 2.0.0 + universalify: 2.0.1 - /fs-extra/9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jsonfile: 6.1.0 - universalify: 2.0.0 + universalify: 2.0.1 - /fs-monkey/1.0.3: - resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} + fs-monkey@1.0.6: {} - /fs.realpath/1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fs.realpath@1.0.0: {} - /fsevents/2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true + fsevents@2.3.3: optional: true - /function-bind/1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + function-bind@1.1.2: {} - /function.prototype.name/1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} + function.prototype.name@1.1.6: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 functions-have-names: 1.2.3 - /functions-have-names/1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + functions-have-names@1.2.3: {} - /gensync/1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} + gensync@1.0.0-beta.2: {} - /get-caller-file/2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} - /get-intrinsic/1.1.3: - resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + get-intrinsic@1.2.5: dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-symbols: 1.0.3 + call-bind-apply-helpers: 1.0.1 + dunder-proto: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + function-bind: 1.1.2 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 - /get-own-enumerable-property-symbols/3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + get-own-enumerable-property-symbols@3.0.2: {} - /get-package-type/0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} + get-package-type@0.1.0: {} - /get-pkg-repo/4.2.1: - resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} - engines: {node: '>=6.9.0'} - hasBin: true - dependencies: - '@hutson/parse-repository-url': 3.0.2 - hosted-git-info: 4.1.0 - through2: 2.0.5 - yargs: 16.2.0 - dev: true + get-stream@6.0.1: {} - /get-stream/6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} + get-stream@8.0.1: {} - /get-symbol-description/1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} + get-symbol-description@1.0.2: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.1.3 + call-bind: 1.0.8 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 - /git-raw-commits/2.0.11: - resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} - engines: {node: '>=10'} - hasBin: true + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - dev: true - - /git-remote-origin-url/2.0.0: - resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} - engines: {node: '>=4'} - dependencies: - gitconfiglocal: 1.0.0 - pify: 2.3.0 - dev: true - - /git-semver-tags/4.1.1: - resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - meow: 8.1.2 - semver: 6.3.0 - dev: true - - /gitconfiglocal/1.0.0: - resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} - dependencies: - ini: 1.3.8 - dev: true - /glob-parent/5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - /glob-parent/6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - /glob-to-regexp/0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob-to-regexp@0.4.1: {} - /glob/7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -6313,248 +11400,146 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 - /global-dirs/0.1.1: - resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} - engines: {node: '>=4'} + global-dirs@0.1.1: dependencies: ini: 1.3.8 - dev: true - - /global-modules/1.0.0: - resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} - engines: {node: '>=0.10.0'} - dependencies: - global-prefix: 1.0.2 - is-windows: 1.0.2 - resolve-dir: 1.0.1 - dev: true - /global-modules/2.0.0: - resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} - engines: {node: '>=6'} + global-modules@2.0.0: dependencies: global-prefix: 3.0.0 - /global-prefix/1.0.2: - resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} - engines: {node: '>=0.10.0'} - dependencies: - expand-tilde: 2.0.2 - homedir-polyfill: 1.0.3 - ini: 1.3.8 - is-windows: 1.0.2 - which: 1.3.1 - dev: true - - /global-prefix/3.0.0: - resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} - engines: {node: '>=6'} + global-prefix@3.0.0: dependencies: ini: 1.3.8 kind-of: 6.0.3 which: 1.3.1 - /globals/11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} + globals@11.12.0: {} - /globals/13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} - engines: {node: '>=8'} + globals@13.24.0: dependencies: type-fest: 0.20.2 - /globby/11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.0 + fast-glob: 3.3.2 + ignore: 5.3.2 merge2: 1.4.1 slash: 3.0.0 - /graceful-fs/4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + gopd@1.2.0: {} - /grapheme-splitter/1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + graceful-fs@4.2.11: {} - /graphlib/2.1.8: - resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} - dependencies: - lodash: 4.17.21 - dev: false + graphemer@1.4.0: {} - /gzip-size/6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 - /handle-thing/2.0.1: - resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + handle-thing@2.0.1: {} - /handlebars/4.7.7: - resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} - engines: {node: '>=0.4.7'} - hasBin: true - dependencies: - minimist: 1.2.6 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.17.1 - dev: true + hard-rejection@2.1.0: {} - /hard-rejection/2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true + harmony-reflect@1.6.2: {} - /harmony-reflect/1.6.2: - resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==} + has-bigints@1.0.2: {} - /has-bigints/1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + has-flag@3.0.0: {} - /has-flag/3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + has-flag@4.0.0: {} - /has-flag/4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 - /has-property-descriptors/1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + has-proto@1.2.0: dependencies: - get-intrinsic: 1.1.3 + dunder-proto: 1.0.0 - /has-symbols/1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + has-symbols@1.1.0: {} - /has-tostringtag/1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: dependencies: - has-symbols: 1.0.3 + has-symbols: 1.1.0 - /has/1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} + hasown@2.0.2: dependencies: - function-bind: 1.1.1 - - /he/1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - /highlight.js/11.6.0: - resolution: {integrity: sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==} - engines: {node: '>=12.0.0'} - dev: false + function-bind: 1.1.2 - /homedir-polyfill/1.0.3: - resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} - engines: {node: '>=0.10.0'} - dependencies: - parse-passwd: 1.0.0 - dev: true + he@1.2.0: {} - /hoopy/0.1.4: - resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} - engines: {node: '>= 6.0.0'} + hoopy@0.1.4: {} - /hosted-git-info/2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + hosted-git-info@2.8.9: {} - /hosted-git-info/4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 - dev: true - /hpack.js/2.1.6: - resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + hpack.js@2.1.6: dependencies: inherits: 2.0.4 obuf: 1.1.2 - readable-stream: 2.3.7 + readable-stream: 2.3.8 wbuf: 1.7.3 - /html-encoding-sniffer/2.0.1: - resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} - engines: {node: '>=10'} + html-encoding-sniffer@2.0.1: dependencies: whatwg-encoding: 1.0.5 - /html-entities/2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + html-entities@2.5.2: {} - /html-escaper/2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@2.0.2: {} - /html-minifier-terser/6.1.0: - resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} - engines: {node: '>=12'} - hasBin: true + html-minifier-terser@6.1.0: dependencies: camel-case: 4.1.2 - clean-css: 5.3.1 + clean-css: 5.3.3 commander: 8.3.0 he: 1.2.0 param-case: 3.0.4 relateurl: 0.2.7 - terser: 5.15.0 + terser: 5.37.0 - /html-parse-stringify/3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 - dev: false - /html-webpack-plugin/5.5.0_webpack@5.74.0: - resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} - engines: {node: '>=10.13.0'} - peerDependencies: - webpack: ^5.20.0 + html-webpack-plugin@5.6.3(webpack@5.97.1): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 lodash: 4.17.21 pretty-error: 4.0.0 tapable: 2.2.1 - webpack: 5.74.0 + optionalDependencies: + webpack: 5.97.1 - /htmlparser2/6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@6.1.0: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 domutils: 2.8.0 entities: 2.2.0 - /http-deceiver/1.2.7: - resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + http-deceiver@1.2.7: {} - /http-errors/1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} + http-errors@1.6.3: dependencies: depd: 1.1.2 inherits: 2.0.3 setprototypeof: 1.1.0 statuses: 1.5.0 - /http-errors/2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} + http-errors@2.0.0: dependencies: depd: 2.0.0 inherits: 2.0.4 @@ -6562,486 +11547,347 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 - /http-parser-js/0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + http-parser-js@0.5.8: {} - /http-proxy-agent/4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} + http-proxy-agent@4.0.1: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color - /http-proxy-middleware/2.0.6_@types+express@4.17.14: - resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/express': ^4.17.13 - peerDependenciesMeta: - '@types/express': - optional: true + http-proxy-middleware@2.0.7(@types/express@4.17.21): dependencies: - '@types/express': 4.17.14 - '@types/http-proxy': 1.17.9 + '@types/http-proxy': 1.17.15 http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 - micromatch: 4.0.5 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.21 transitivePeerDependencies: - debug - /http-proxy/1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} + http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.2 + follow-redirects: 1.15.9 requires-port: 1.0.0 transitivePeerDependencies: - debug - /https-proxy-agent/5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color - /human-signals/2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - /human-signals/3.0.1: - resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} - engines: {node: '>=12.20.0'} - dev: true - - /husky/8.0.1: - resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /i18next-chained-backend/3.1.0: - resolution: {integrity: sha512-ltWy0fPMrtXjq0rSLo7s4ZF92LGvVsO47hhV3czaZXzMzhpFva6LVKMyDT7x82R8vAyB7VAeyGLBvMiW4W543A==} - dependencies: - '@babel/runtime': 7.19.0 - dev: false + human-signals@2.1.0: {} - /i18next-http-backend/1.4.4: - resolution: {integrity: sha512-M4gLPe6JKZ2p1UmE6t4rzWV/sAxgrLThW7ztXAsTpFwFqXoyzhTzX8eYxVv9KjpCQh4K9nwxnEjEi+74C4Thbg==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false + human-signals@5.0.0: {} - /i18next-localstorage-backend/3.1.3: - resolution: {integrity: sha512-tx8dxQTEsTnRC654IrXPFr94c3NH7bIVHGKHnGvbgefpLz13/uFT5ITsmhqhg/gOza0TIj8e5jTsGnQytIhh+A==} - dependencies: - '@babel/runtime': 7.19.0 - dev: false + husky@9.1.7: {} - /i18next/21.9.2: - resolution: {integrity: sha512-00fVrLQOwy45nm3OtC9l1WiLK3nJlIYSljgCt0qzTaAy65aciMdRy9GsuW+a2AtKtdg9/njUGfRH30LRupV7ZQ==} + i18next@21.10.0: dependencies: - '@babel/runtime': 7.19.0 - dev: false + '@babel/runtime': 7.26.0 - /iconv-lite/0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - /iconv-lite/0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - /icss-utils/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + icss-utils@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /idb/7.0.2: - resolution: {integrity: sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==} + idb@7.1.1: {} - /identity-obj-proxy/3.0.0: - resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} - engines: {node: '>=4'} + identity-obj-proxy@3.0.0: dependencies: harmony-reflect: 1.6.2 - /ieee754/1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true + ieee754@1.2.1: {} - /ignore/5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} - engines: {node: '>= 4'} + ignore@5.3.2: {} - /immer/9.0.15: - resolution: {integrity: sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==} + immer@9.0.21: {} - /immutable/4.1.0: - resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==} + immutable@4.3.7: {} - /import-fresh/3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - /import-local/3.1.0: - resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} - engines: {node: '>=8'} - hasBin: true + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 - /imurmurhash/0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} + imurmurhash@0.1.4: {} - /indent-string/4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} + indent-string@4.0.0: {} - /inflight/1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - /inherits/2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - - /inherits/2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - /ini/1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - /inquirer/8.2.4: - resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} - engines: {node: '>=12.0.0'} - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.5.6 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true - - /internal-slot/1.0.3: - resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.1.3 - has: 1.0.3 - side-channel: 1.0.4 + inherits@2.0.3: {} - /internmap/2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - dev: false + inherits@2.0.4: {} - /intersection-observer/0.12.2: - resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} - dev: false + ini@1.3.8: {} - /invariant/2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + + invariant@2.2.4: dependencies: loose-envify: 1.4.0 - dev: false - /ipaddr.js/1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + ipaddr.js@1.9.1: {} - /ipaddr.js/2.0.1: - resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} - engines: {node: '>= 10'} + ipaddr.js@2.2.0: {} - /is-arrayish/0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.8 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-async-function@2.0.0: + dependencies: + has-tostringtag: 1.0.2 - /is-bigint/1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-bigint@1.1.0: dependencies: has-bigints: 1.0.2 - /is-binary-path/2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + is-binary-path@2.1.0: dependencies: - binary-extensions: 2.2.0 + binary-extensions: 2.3.0 - /is-boolean-object/1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + is-boolean-object@1.2.0: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 + call-bind: 1.0.8 + has-tostringtag: 1.0.2 - /is-callable/1.2.6: - resolution: {integrity: sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==} - engines: {node: '>= 0.4'} + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} - /is-core-module/2.10.0: - resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + is-core-module@2.15.1: dependencies: - has: 1.0.3 + hasown: 2.0.2 - /is-date-object/1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} + is-data-view@1.0.1: dependencies: - has-tostringtag: 1.0.0 + is-typed-array: 1.1.13 - /is-docker/2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true + is-date-object@1.0.5: + dependencies: + has-tostringtag: 1.0.2 - /is-extglob/2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + is-docker@2.2.1: {} - /is-fullwidth-code-point/3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + is-extglob@2.1.1: {} - /is-fullwidth-code-point/4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true + is-finalizationregistry@1.1.0: + dependencies: + call-bind: 1.0.8 - /is-generator-fn/2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} + is-fullwidth-code-point@3.0.0: {} - /is-glob/4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-generator-fn@2.1.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - /is-interactive/1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true + is-map@2.0.3: {} - /is-module/1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-module@1.0.0: {} - /is-negative-zero/2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: {} - /is-number-object/1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + is-number-object@1.1.0: dependencies: - has-tostringtag: 1.0.0 + call-bind: 1.0.8 + has-tostringtag: 1.0.2 - /is-number/7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + is-number@7.0.0: {} - /is-obj/1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} + is-obj@1.0.1: {} - /is-obj/2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - dev: true + is-obj@2.0.0: {} - /is-plain-obj/1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true + is-path-inside@3.0.3: {} - /is-plain-obj/3.0.0: - resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} - engines: {node: '>=10'} + is-plain-obj@1.1.0: {} - /is-potential-custom-element-name/1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-plain-obj@3.0.0: {} - /is-regex/1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.0: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 + call-bind: 1.0.8 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 - /is-regexp/1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} + is-regexp@1.0.0: {} - /is-root/2.1.0: - resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} - engines: {node: '>=6'} + is-root@2.1.0: {} - /is-shared-array-buffer/1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.3: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.8 - /is-stream/2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + is-stream@2.0.1: {} - /is-stream/3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + is-stream@3.0.0: {} - /is-string/1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + is-string@1.1.0: dependencies: - has-tostringtag: 1.0.0 + call-bind: 1.0.8 + has-tostringtag: 1.0.2 - /is-symbol/1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} + is-symbol@1.1.0: dependencies: - has-symbols: 1.0.3 + call-bind: 1.0.8 + has-symbols: 1.1.0 + safe-regex-test: 1.0.3 - /is-text-path/1.0.1: - resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} - engines: {node: '>=0.10.0'} + is-text-path@1.0.1: dependencies: text-extensions: 1.9.0 - dev: true - /is-typedarray/1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.16 - /is-unicode-supported/0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true + is-typedarray@1.0.0: {} - /is-utf8/0.2.1: - resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} - dev: true + is-weakmap@2.0.2: {} - /is-weakref/1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-weakref@1.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.8 - /is-windows/1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - dev: true + is-weakset@2.0.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.2.5 - /is-wsl/2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 - /isarray/1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@1.0.0: {} - /isexe/2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isarray@2.0.5: {} - /istanbul-lib-coverage/3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} + isexe@2.0.0: {} - /istanbul-lib-instrument/5.2.0: - resolution: {integrity: sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==} - engines: {node: '>=8'} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.19.1 - '@babel/parser': 7.19.1 + '@babel/core': 7.26.0 + '@babel/parser': 7.26.3 '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.0 - semver: 6.3.0 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 transitivePeerDependencies: - supports-color - /istanbul-lib-report/3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} + istanbul-lib-report@3.0.1: dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 supports-color: 7.2.0 - /istanbul-lib-source-maps/4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.4 - istanbul-lib-coverage: 3.2.0 + debug: 4.4.0 + istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color - /istanbul-reports/3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 + istanbul-lib-report: 3.0.1 - /jake/10.8.5: - resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} - engines: {node: '>=10'} - hasBin: true + iterator.prototype@1.1.3: + dependencies: + define-properties: 1.2.1 + get-intrinsic: 1.2.5 + has-symbols: 1.1.0 + reflect.getprototypeof: 1.0.8 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: dependencies: - async: 3.2.4 + async: 3.2.6 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 - /jest-changed-files/27.5.1: - resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + javascript-stringify@2.1.0: {} + + jest-changed-files@27.5.1: dependencies: '@jest/types': 27.5.1 execa: 5.1.1 - throat: 6.0.1 + throat: 6.0.2 - /jest-circus/27.5.1: - resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-circus@27.5.1: dependencies: '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -7055,29 +11901,21 @@ packages: jest-util: 27.5.1 pretty-format: 27.5.1 slash: 3.0.0 - stack-utils: 2.0.5 - throat: 6.0.1 + stack-utils: 2.0.6 + throat: 6.0.2 transitivePeerDependencies: - supports-color - /jest-cli/27.5.1: - resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jest-cli@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)): dependencies: - '@jest/core': 27.5.1 + '@jest/core': 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 chalk: 4.1.2 exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 27.5.1 + graceful-fs: 4.2.11 + import-local: 3.2.0 + jest-config: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) jest-util: 27.5.1 jest-validate: 27.5.1 prompts: 2.4.2 @@ -7089,24 +11927,17 @@ packages: - ts-node - utf-8-validate - /jest-config/27.5.1: - resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - peerDependencies: - ts-node: '>=9.0.0' - peerDependenciesMeta: - ts-node: - optional: true + jest-config@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)): dependencies: - '@babel/core': 7.19.1 + '@babel/core': 7.26.0 '@jest/test-sequencer': 27.5.1 '@jest/types': 27.5.1 - babel-jest: 27.5.1_@babel+core@7.19.1 + babel-jest: 27.5.1(@babel/core@7.26.0) chalk: 4.1.2 - ci-info: 3.4.0 - deepmerge: 4.2.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-circus: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -7117,45 +11948,38 @@ packages: jest-runner: 27.5.1 jest-util: 27.5.1 jest-validate: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 parse-json: 5.2.0 pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + ts-node: 10.9.2(@types/node@20.5.1)(typescript@4.9.5) transitivePeerDependencies: - bufferutil - canvas - supports-color - utf-8-validate - /jest-diff/24.9.0: - resolution: {integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==} - engines: {node: '>= 6'} + jest-diff@24.9.0: dependencies: chalk: 2.4.2 diff-sequences: 24.9.0 jest-get-type: 24.9.0 pretty-format: 24.9.0 - dev: false - /jest-diff/27.5.1: - resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-diff@27.5.1: dependencies: chalk: 4.1.2 diff-sequences: 27.5.1 jest-get-type: 27.5.1 pretty-format: 27.5.1 - /jest-docblock/27.5.1: - resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-docblock@27.5.1: dependencies: detect-newline: 3.1.0 - /jest-each/27.5.1: - resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-each@27.5.1: dependencies: '@jest/types': 27.5.1 chalk: 4.1.2 @@ -7163,14 +11987,12 @@ packages: jest-util: 27.5.1 pretty-format: 27.5.1 - /jest-environment-jsdom/27.5.1: - resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-environment-jsdom@27.5.1: dependencies: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 jest-mock: 27.5.1 jest-util: 27.5.1 jsdom: 16.7.0 @@ -7180,54 +12002,43 @@ packages: - supports-color - utf-8-validate - /jest-environment-node/27.5.1: - resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-environment-node@27.5.1: dependencies: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 jest-mock: 27.5.1 jest-util: 27.5.1 - /jest-get-type/24.9.0: - resolution: {integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==} - engines: {node: '>= 6'} - dev: false + jest-get-type@24.9.0: {} - /jest-get-type/27.5.1: - resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-get-type@27.5.1: {} - /jest-haste-map/27.5.1: - resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-haste-map@27.5.1: dependencies: '@jest/types': 27.5.1 - '@types/graceful-fs': 4.1.5 - '@types/node': 16.11.59 - anymatch: 3.1.2 - fb-watchman: 2.0.1 - graceful-fs: 4.2.10 + '@types/graceful-fs': 4.1.9 + '@types/node': 16.18.121 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 jest-regex-util: 27.5.1 jest-serializer: 27.5.1 jest-util: 27.5.1 jest-worker: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 - /jest-jasmine2/27.5.1: - resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-jasmine2@27.5.1: dependencies: '@jest/environment': 27.5.1 '@jest/source-map': 27.5.1 '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 chalk: 4.1.2 co: 4.6.0 expect: 27.5.1 @@ -7239,93 +12050,67 @@ packages: jest-snapshot: 27.5.1 jest-util: 27.5.1 pretty-format: 27.5.1 - throat: 6.0.1 + throat: 6.0.2 transitivePeerDependencies: - supports-color - /jest-leak-detector/27.5.1: - resolution: {integrity: sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-leak-detector@27.5.1: dependencies: jest-get-type: 27.5.1 pretty-format: 27.5.1 - /jest-matcher-utils/24.9.0: - resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} - engines: {node: '>= 6'} + jest-matcher-utils@24.9.0: dependencies: chalk: 2.4.2 jest-diff: 24.9.0 jest-get-type: 24.9.0 pretty-format: 24.9.0 - dev: false - /jest-matcher-utils/27.5.1: - resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-matcher-utils@27.5.1: dependencies: chalk: 4.1.2 jest-diff: 27.5.1 jest-get-type: 27.5.1 pretty-format: 27.5.1 - /jest-message-util/27.5.1: - resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-message-util@27.5.1: dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.26.2 '@jest/types': 27.5.1 - '@types/stack-utils': 2.0.1 + '@types/stack-utils': 2.0.3 chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 + graceful-fs: 4.2.11 + micromatch: 4.0.8 pretty-format: 27.5.1 slash: 3.0.0 - stack-utils: 2.0.5 + stack-utils: 2.0.6 - /jest-message-util/28.1.3: - resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-message-util@28.1.3: dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.26.2 '@jest/types': 28.1.3 - '@types/stack-utils': 2.0.1 + '@types/stack-utils': 2.0.3 chalk: 4.1.2 - graceful-fs: 4.2.10 - micromatch: 4.0.5 + graceful-fs: 4.2.11 + micromatch: 4.0.8 pretty-format: 28.1.3 slash: 3.0.0 - stack-utils: 2.0.5 + stack-utils: 2.0.6 - /jest-mock/27.5.1: - resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-mock@27.5.1: dependencies: '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 - /jest-pnp-resolver/1.2.2_jest-resolve@27.5.1: - resolution: {integrity: sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - dependencies: + jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): + optionalDependencies: jest-resolve: 27.5.1 - /jest-regex-util/27.5.1: - resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-regex-util@27.5.1: {} - /jest-regex-util/28.0.2: - resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-regex-util@28.0.2: {} - /jest-resolve-dependencies/27.5.1: - resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-resolve-dependencies@27.5.1: dependencies: '@jest/types': 27.5.1 jest-regex-util: 27.5.1 @@ -7333,34 +12118,30 @@ packages: transitivePeerDependencies: - supports-color - /jest-resolve/27.5.1: - resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-resolve@27.5.1: dependencies: '@jest/types': 27.5.1 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 - jest-pnp-resolver: 1.2.2_jest-resolve@27.5.1 + jest-pnp-resolver: 1.2.3(jest-resolve@27.5.1) jest-util: 27.5.1 jest-validate: 27.5.1 - resolve: 1.22.1 - resolve.exports: 1.1.0 + resolve: 1.22.8 + resolve.exports: 1.1.1 slash: 3.0.0 - /jest-runner/27.5.1: - resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-runner@27.5.1: dependencies: '@jest/console': 27.5.1 '@jest/environment': 27.5.1 '@jest/test-result': 27.5.1 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 chalk: 4.1.2 emittery: 0.8.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-docblock: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -7372,16 +12153,14 @@ packages: jest-util: 27.5.1 jest-worker: 27.5.1 source-map-support: 0.5.21 - throat: 6.0.1 + throat: 6.0.2 transitivePeerDependencies: - bufferutil - canvas - supports-color - utf-8-validate - /jest-runtime/27.5.1: - resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-runtime@27.5.1: dependencies: '@jest/environment': 27.5.1 '@jest/fake-timers': 27.5.1 @@ -7391,11 +12170,11 @@ packages: '@jest/transform': 27.5.1 '@jest/types': 27.5.1 chalk: 4.1.2 - cjs-module-lexer: 1.2.2 - collect-v8-coverage: 1.0.1 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 execa: 5.1.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 27.5.1 jest-message-util: 27.5.1 jest-mock: 27.5.1 @@ -7408,30 +12187,26 @@ packages: transitivePeerDependencies: - supports-color - /jest-serializer/27.5.1: - resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-serializer@27.5.1: dependencies: - '@types/node': 16.11.59 - graceful-fs: 4.2.10 + '@types/node': 16.18.121 + graceful-fs: 4.2.11 - /jest-snapshot/27.5.1: - resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-snapshot@27.5.1: dependencies: - '@babel/core': 7.19.1 - '@babel/generator': 7.19.0 - '@babel/plugin-syntax-typescript': 7.18.6_@babel+core@7.19.1 - '@babel/traverse': 7.19.1 - '@babel/types': 7.19.0 + '@babel/core': 7.26.0 + '@babel/generator': 7.26.3 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/traverse': 7.26.4 + '@babel/types': 7.26.3 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 - '@types/babel__traverse': 7.18.1 - '@types/prettier': 2.7.0 - babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.1 + '@types/babel__traverse': 7.20.6 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) chalk: 4.1.2 expect: 27.5.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-diff: 27.5.1 jest-get-type: 27.5.1 jest-haste-map: 27.5.1 @@ -7440,35 +12215,29 @@ packages: jest-util: 27.5.1 natural-compare: 1.4.0 pretty-format: 27.5.1 - semver: 7.3.7 + semver: 7.6.3 transitivePeerDependencies: - supports-color - /jest-util/27.5.1: - resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-util@27.5.1: dependencies: '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 chalk: 4.1.2 - ci-info: 3.4.0 - graceful-fs: 4.2.10 + ci-info: 3.9.0 + graceful-fs: 4.2.11 picomatch: 2.3.1 - /jest-util/28.1.3: - resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-util@28.1.3: dependencies: '@jest/types': 28.1.3 - '@types/node': 16.11.59 + '@types/node': 16.18.121 chalk: 4.1.2 - ci-info: 3.4.0 - graceful-fs: 4.2.10 + ci-info: 3.9.0 + graceful-fs: 4.2.11 picomatch: 2.3.1 - /jest-validate/27.5.1: - resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-validate@27.5.1: dependencies: '@jest/types': 27.5.1 camelcase: 6.3.0 @@ -7477,83 +12246,61 @@ packages: leven: 3.1.0 pretty-format: 27.5.1 - /jest-watch-typeahead/1.1.0_jest@27.5.1: - resolution: {integrity: sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - jest: ^27.0.0 || ^28.0.0 + jest-watch-typeahead@1.1.0(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 - jest: 27.5.1 + jest: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) jest-regex-util: 28.0.2 jest-watcher: 28.1.3 slash: 4.0.0 string-length: 5.0.1 - strip-ansi: 7.0.1 + strip-ansi: 7.1.0 - /jest-watcher/27.5.1: - resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + jest-watcher@27.5.1: dependencies: '@jest/test-result': 27.5.1 '@jest/types': 27.5.1 - '@types/node': 16.11.59 + '@types/node': 16.18.121 ansi-escapes: 4.3.2 chalk: 4.1.2 jest-util: 27.5.1 string-length: 4.0.2 - /jest-watcher/28.1.3: - resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-watcher@28.1.3: dependencies: '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 16.11.59 + '@types/node': 16.18.121 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 jest-util: 28.1.3 string-length: 4.0.2 - /jest-worker/26.6.2: - resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} - engines: {node: '>= 10.13.0'} + jest-worker@26.6.2: dependencies: - '@types/node': 16.11.59 + '@types/node': 16.18.121 merge-stream: 2.0.0 supports-color: 7.2.0 - /jest-worker/27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} + jest-worker@27.5.1: dependencies: - '@types/node': 16.11.59 + '@types/node': 16.18.121 merge-stream: 2.0.0 supports-color: 8.1.1 - /jest-worker/28.1.3: - resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + jest-worker@28.1.3: dependencies: - '@types/node': 16.11.59 + '@types/node': 16.18.121 merge-stream: 2.0.0 supports-color: 8.1.1 - /jest/27.5.1: - resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)): dependencies: - '@jest/core': 27.5.1 - import-local: 3.1.0 - jest-cli: 27.5.1 + '@jest/core': 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) + import-local: 3.2.0 + jest-cli: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) transitivePeerDependencies: - bufferutil - canvas @@ -7561,434 +12308,287 @@ packages: - ts-node - utf-8-validate - /js-cookie/2.2.1: - resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} - dev: false + jiti@1.21.6: {} - /js-sdsl/4.1.4: - resolution: {integrity: sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==} + js-sha256@0.11.0: {} - /js-tokens/4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@4.0.0: {} - /js-yaml/3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true + js-yaml@3.14.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - /js-yaml/4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.0: dependencies: argparse: 2.0.1 - /jsdom/16.7.0: - resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} - engines: {node: '>=10'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true + jsdom@16.7.0: dependencies: abab: 2.0.6 - acorn: 8.8.0 + acorn: 8.14.0 acorn-globals: 6.0.0 cssom: 0.4.4 cssstyle: 2.3.0 data-urls: 2.0.0 - decimal.js: 10.4.1 + decimal.js: 10.4.3 domexception: 2.0.1 - escodegen: 2.0.0 - form-data: 3.0.1 + escodegen: 2.1.0 + form-data: 3.0.2 html-encoding-sniffer: 2.0.1 http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.2 + nwsapi: 2.2.16 parse5: 6.0.1 saxes: 5.0.1 symbol-tree: 3.2.4 - tough-cookie: 4.1.2 + tough-cookie: 4.1.4 w3c-hr-time: 1.0.2 w3c-xmlserializer: 2.0.0 webidl-conversions: 6.1.0 whatwg-encoding: 1.0.5 whatwg-mimetype: 2.3.0 whatwg-url: 8.7.0 - ws: 7.5.9 + ws: 7.5.10 xml-name-validator: 3.0.0 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - /jsesc/0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - - /jsesc/2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - /json-parse-better-errors/1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - dev: true + jsesc@3.0.2: {} - /json-parse-even-better-errors/2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-buffer@3.0.1: {} - /json-schema-traverse/0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-parse-even-better-errors@2.3.1: {} - /json-schema-traverse/1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-traverse@0.4.1: {} - /json-schema/0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-schema-traverse@1.0.0: {} - /json-stable-stringify-without-jsonify/1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-schema@0.4.0: {} - /json-stringify-safe/5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true + json-stable-stringify-without-jsonify@1.0.1: {} - /json5/1.0.1: - resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} - hasBin: true + json5@1.0.2: dependencies: - minimist: 1.2.6 + minimist: 1.2.8 - /json5/2.2.1: - resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} - engines: {node: '>=6'} - hasBin: true + json5@2.2.3: {} - /jsonfile/6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.1.0: dependencies: - universalify: 2.0.0 + universalify: 2.0.1 optionalDependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 - /jsonp/0.2.1: - resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==} + jsonp@0.2.1: dependencies: debug: 2.6.9 transitivePeerDependencies: - supports-color - dev: false - /jsonparse/1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - dev: true + jsonparse@1.3.1: {} - /jsonpointer/5.0.1: - resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} - engines: {node: '>=0.10.0'} + jsonpath@1.1.1: + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 - /jsx-ast-utils/3.3.3: - resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} - engines: {node: '>=4.0'} + jsonpointer@5.0.1: {} + + jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.5 - object.assign: 4.1.4 + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 - /katex/0.16.2: - resolution: {integrity: sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==} - hasBin: true + keyv@4.5.4: dependencies: - commander: 8.3.0 - dev: false + json-buffer: 3.0.1 - /khroma/2.0.0: - resolution: {integrity: sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==} - dev: false + kind-of@6.0.3: {} - /kind-of/6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} + kleur@3.0.3: {} - /kleur/3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} + klona@2.0.6: {} - /klona/2.0.5: - resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} - engines: {node: '>= 8'} + language-subtag-registry@0.3.23: {} - /language-subtag-registry/0.3.22: - resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 - /language-tags/1.0.5: - resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + launch-editor@2.9.1: dependencies: - language-subtag-registry: 0.3.22 + picocolors: 1.1.1 + shell-quote: 1.8.2 - /leven/3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} + leven@3.1.0: {} - /levn/0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} + levn@0.3.0: dependencies: prelude-ls: 1.1.2 type-check: 0.3.2 - - /levn/0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - /lilconfig/2.0.5: - resolution: {integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==} - engines: {node: '>=10'} - dev: true + lilconfig@2.1.0: {} - /lilconfig/2.0.6: - resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} - engines: {node: '>=10'} + lilconfig@3.1.3: {} - /lines-and-columns/1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lines-and-columns@1.2.4: {} - /lint-staged/13.0.3: - resolution: {integrity: sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==} - engines: {node: ^14.13.1 || >=16.0.0} - hasBin: true + lint-staged@15.5.0: dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.19 - commander: 9.4.0 - debug: 4.3.4 - execa: 6.1.0 - lilconfig: 2.0.5 - listr2: 4.0.5 - micromatch: 4.0.5 - normalize-path: 3.0.0 - object-inspect: 1.12.2 + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.0 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.2.5 + micromatch: 4.0.8 pidtree: 0.6.0 - string-argv: 0.3.1 - yaml: 2.1.1 + string-argv: 0.3.2 + yaml: 2.7.0 transitivePeerDependencies: - - enquirer - supports-color - dev: true - - /listr2/4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.19 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.3.0 - rxjs: 7.5.6 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true - /load-json-file/4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} + listr2@8.2.5: dependencies: - graceful-fs: 4.2.10 - parse-json: 4.0.0 - pify: 3.0.0 - strip-bom: 3.0.0 - dev: true + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 - /loader-runner/4.3.0: - resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} - engines: {node: '>=6.11.5'} + loader-runner@4.3.0: {} - /loader-utils/2.0.2: - resolution: {integrity: sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==} - engines: {node: '>=8.9.0'} + loader-utils@2.0.4: dependencies: big.js: 5.2.2 emojis-list: 3.0.0 - json5: 2.2.1 - - /loader-utils/3.2.0: - resolution: {integrity: sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==} - engines: {node: '>= 12.13.0'} + json5: 2.2.3 - /locate-path/2.0.0: - resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} - engines: {node: '>=4'} - dependencies: - p-locate: 2.0.0 - path-exists: 3.0.0 - dev: true + loader-utils@3.3.1: {} - /locate-path/3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} + locate-path@3.0.0: dependencies: p-locate: 3.0.0 path-exists: 3.0.0 - /locate-path/5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 - /locate-path/6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - /lodash.debounce/4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.camelcase@4.3.0: {} - /lodash.flow/3.5.0: - resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} - dev: true + lodash.debounce@4.0.8: {} - /lodash.ismatch/4.4.0: - resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} - dev: true + lodash.flow@3.5.0: {} - /lodash.map/4.6.0: - resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} - dev: true + lodash.isfunction@3.0.9: {} - /lodash.memoize/4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.isplainobject@4.0.6: {} - /lodash.merge/4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.kebabcase@4.1.1: {} - /lodash.sortby/4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.memoize@4.1.2: {} - /lodash.uniq/4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash.merge@4.6.2: {} - /lodash/4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash.mergewith@4.6.2: {} - /log-symbols/4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: true + lodash.snakecase@4.1.1: {} - /log-update/4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - dev: true + lodash.sortby@4.7.0: {} - /longest/2.0.1: - resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} - engines: {node: '>=0.10.0'} - dev: true + lodash.startcase@4.4.0: {} - /loose-envify/1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - /lower-case/2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lower-case@2.0.2: dependencies: - tslib: 2.4.0 + tslib: 2.8.1 - /lru-cache/6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: dependencies: yallist: 4.0.0 - /lz-string/1.4.4: - resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} - hasBin: true - dev: true + lz-string@1.5.0: {} - /magic-string/0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 - /make-dir/3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} + make-dir@3.1.0: dependencies: - semver: 6.3.0 + semver: 6.3.1 - /make-error/1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true + make-dir@4.0.0: + dependencies: + semver: 7.6.3 - /makeerror/1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + make-error@1.3.6: {} + + makeerror@1.0.12: dependencies: tmpl: 1.0.5 - /map-obj/1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: true + map-obj@1.0.1: {} - /map-obj/4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true + map-obj@4.3.0: {} - /marked/4.1.0: - resolution: {integrity: sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==} - engines: {node: '>= 12'} - hasBin: true - dev: false + marked@4.3.0: {} - /mdn-data/2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdn-data@2.0.14: {} - /mdn-data/2.0.4: - resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + mdn-data@2.0.4: {} - /media-typer/0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + media-typer@0.3.0: {} - /memfs/3.4.7: - resolution: {integrity: sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==} - engines: {node: '>= 4.0.0'} + memfs@3.5.3: dependencies: - fs-monkey: 1.0.3 + fs-monkey: 1.0.6 - /meow/8.1.2: - resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} - engines: {node: '>=10'} + meow@8.1.2: dependencies: - '@types/minimist': 1.2.2 + '@types/minimist': 1.2.5 camelcase-keys: 6.2.2 - decamelize-keys: 1.1.0 + decamelize-keys: 1.1.1 hard-rejection: 2.1.0 minimist-options: 4.1.0 normalize-package-data: 3.0.3 @@ -7997,1730 +12597,1103 @@ packages: trim-newlines: 3.0.1 type-fest: 0.18.1 yargs-parser: 20.2.9 - dev: true - - /merge-descriptors/1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - /merge-stream/2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge-descriptors@1.0.3: {} - /merge/2.1.1: - resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} - dev: true + merge-stream@2.0.0: {} - /merge2/1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + merge2@1.4.1: {} - /mermaid/9.1.7: - resolution: {integrity: sha512-MRVHXy5FLjnUQUG7YS3UN9jEN6FXCJbFCXVGJQjVIbiR6Vhw0j/6pLIjqsiah9xoHmQU6DEaKOvB3S1g/1nBPA==} - dependencies: - '@braintree/sanitize-url': 6.0.0 - d3: 7.6.1 - dagre: 0.8.5 - dagre-d3: 0.6.4 - dompurify: 2.4.0 - graphlib: 2.1.8 - khroma: 2.0.0 - moment-mini: 2.24.0 - stylis: 4.1.2 - dev: false - - /methods/1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + methods@1.1.2: {} - /micromatch/4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + micromatch@4.0.8: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 - /mime-db/1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + mime-db@1.52.0: {} - /mime-types/2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-db@1.53.0: {} + + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - /mime/1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mime@1.6.0: {} - /mimic-fn/2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + mimic-fn@2.1.0: {} - /mimic-fn/4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true + mimic-fn@4.0.0: {} - /min-indent/1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} + mimic-function@5.0.1: {} - /mini-css-extract-plugin/2.6.1_webpack@5.74.0: - resolution: {integrity: sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 + min-indent@1.0.1: {} + + mini-css-extract-plugin@2.9.2(webpack@5.97.1): dependencies: - schema-utils: 4.0.0 - webpack: 5.74.0 + schema-utils: 4.2.0 + tapable: 2.2.1 + webpack: 5.97.1 - /minimalistic-assert/1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimalistic-assert@1.0.1: {} - /minimatch/3.0.4: - resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - /minimatch/3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 2.0.1 - /minimatch/5.1.0: - resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==} - engines: {node: '>=10'} + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 - /minimist-options/4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist-options@4.1.0: dependencies: arrify: 1.0.1 is-plain-obj: 1.1.0 kind-of: 6.0.3 - dev: true - - /minimist/1.2.6: - resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} - - /mkdirp/0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - dependencies: - minimist: 1.2.6 - /modify-values/1.0.1: - resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} - engines: {node: '>=0.10.0'} - dev: true + minimist@1.2.8: {} - /moment-mini/2.24.0: - resolution: {integrity: sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==} - dev: false + minipass@7.1.2: {} - /ms/2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 - /ms/2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.0.0: {} - /ms/2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + ms@2.1.3: {} - /multicast-dns/7.2.5: - resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} - hasBin: true + multicast-dns@7.2.5: dependencies: - dns-packet: 5.4.0 + dns-packet: 5.6.1 thunky: 1.1.0 - /mute-stream/0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.8: {} - /nanoid/3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + natural-compare-lite@1.4.0: {} - /natural-compare/1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + natural-compare@1.4.0: {} - /negotiator/0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + negotiator@0.6.3: {} - /neo-async/2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + negotiator@0.6.4: {} - /next-share/0.18.1_lbqamd2wfmenkveygahn4wdfcq: - resolution: {integrity: sha512-M7+J8ShJxVHQymKDq7NQCtcCemWL1bMQ3QNtu7rEr1ouDO4wzlhHeLG5vldfsI/JhUwnrOKYAhJ/UL26QpL+zg==} - engines: {node: '>=8', npm: '>=5'} - peerDependencies: - react: '>=17.0.0' - react-dom: '>=17.0.0' - react-scripts: '>=4.0.0' + neo-async@2.6.2: {} + + next-share@0.18.4(react@18.3.1): dependencies: jsonp: 0.2.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i + react: 18.3.1 transitivePeerDependencies: - supports-color - dev: false - /no-case/3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.4.0 - - /node-fetch/2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false + tslib: 2.8.1 - /node-forge/1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} + node-forge@1.3.1: {} - /node-int64/0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-int64@0.4.0: {} - /node-releases/2.0.6: - resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} + node-releases@2.0.19: {} - /normalize-package-data/2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.1 - semver: 5.7.1 + resolve: 1.22.8 + semver: 5.7.2 validate-npm-package-license: 3.0.4 - dev: true - /normalize-package-data/3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} + normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 - is-core-module: 2.10.0 - semver: 7.3.7 + is-core-module: 2.15.1 + semver: 7.6.3 validate-npm-package-license: 3.0.4 - dev: true - /normalize-path/3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} + normalize-path@3.0.0: {} - /normalize-range/0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} + normalize-range@0.1.2: {} - /normalize-url/6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} + normalize-url@6.1.0: {} - /npm-run-path/4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 - /npm-run-path/5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 - dev: true - /nth-check/1.0.2: - resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + nth-check@1.0.2: dependencies: boolbase: 1.0.0 - /nth-check/2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 - /nwsapi/2.2.2: - resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + nwsapi@2.2.16: {} - /object-assign/4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} + object-assign@4.1.1: {} - /object-hash/3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} + object-hash@3.0.0: {} - /object-inspect/1.12.2: - resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + object-inspect@1.13.3: {} - /object-keys/1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 - /object.assign/4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} + object-keys@1.1.1: {} + + object.assign@4.1.5: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - has-symbols: 1.0.3 + call-bind: 1.0.8 + define-properties: 1.2.1 + has-symbols: 1.1.0 object-keys: 1.1.1 - /object.entries/1.1.5: - resolution: {integrity: sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==} - engines: {node: '>= 0.4'} + object.entries@1.1.8: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 - /object.fromentries/2.0.5: - resolution: {integrity: sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==} - engines: {node: '>= 0.4'} + object.fromentries@2.0.8: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 - /object.getownpropertydescriptors/2.1.4: - resolution: {integrity: sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==} - engines: {node: '>= 0.8'} + object.getownpropertydescriptors@2.1.8: dependencies: - array.prototype.reduce: 1.0.4 - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + array.prototype.reduce: 1.0.7 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + gopd: 1.2.0 + safe-array-concat: 1.1.2 - /object.hasown/1.1.1: - resolution: {integrity: sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==} + object.groupby@1.0.3: dependencies: - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 - /object.values/1.1.5: - resolution: {integrity: sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==} - engines: {node: '>= 0.4'} + object.values@1.2.0: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 - /obuf/1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obuf@1.1.2: {} - /on-finished/2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 - /on-headers/1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} + on-headers@1.0.2: {} - /once/1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - /onetime/5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - /onetime/6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 - dev: true - /open/8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 - /optionator/0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} + optionator@0.8.3: dependencies: deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.3.0 prelude-ls: 1.1.2 type-check: 0.3.2 - word-wrap: 1.2.3 + word-wrap: 1.2.5 - /optionator/0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} + optionator@0.9.4: dependencies: deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - word-wrap: 1.2.3 - - /ora/5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.7.0 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: true - - /os-tmpdir/1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true - - /p-limit/1.3.0: - resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} - engines: {node: '>=4'} - dependencies: - p-try: 1.0.0 - dev: true + word-wrap: 1.2.5 - /p-limit/2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + p-limit@2.3.0: dependencies: p-try: 2.2.0 - /p-limit/3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - /p-locate/2.0.0: - resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} - engines: {node: '>=4'} - dependencies: - p-limit: 1.3.0 - dev: true - - /p-locate/3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} + p-locate@3.0.0: dependencies: p-limit: 2.3.0 - /p-locate/4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + p-locate@4.1.0: dependencies: p-limit: 2.3.0 - /p-locate/5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - /p-map/4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: true - - /p-retry/4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 - /p-try/1.0.0: - resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} - engines: {node: '>=4'} - dev: true + p-try@2.2.0: {} - /p-try/2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} + package-json-from-dist@1.0.1: {} - /param-case/3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + param-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.4.0 + tslib: 2.8.1 - /parent-module/1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - /parse-json/4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - dependencies: - error-ex: 1.3.2 - json-parse-better-errors: 1.0.2 - dev: true - - /parse-json/5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - /parse-passwd/1.0.0: - resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} - engines: {node: '>=0.10.0'} - dev: true - - /parse5/6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@6.0.1: {} - /parseurl/1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + parseurl@1.3.3: {} - /pascal-case/3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + pascal-case@3.1.2: dependencies: no-case: 3.0.4 - tslib: 2.4.0 + tslib: 2.8.1 - /path-exists/3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - /path-exists/4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + path-exists@3.0.0: {} - /path-is-absolute/1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + path-exists@4.0.0: {} - /path-key/3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + path-is-absolute@1.0.1: {} - /path-key/4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true + path-key@3.1.1: {} - /path-parse/1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-key@4.0.0: {} - /path-to-regexp/0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-parse@1.0.7: {} - /path-type/3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} + path-scurry@1.11.1: dependencies: - pify: 3.0.0 - dev: true + lru-cache: 10.4.3 + minipass: 7.1.2 - /path-type/4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + path-to-regexp@0.1.12: {} - /performance-now/2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + path-type@4.0.0: {} - /picocolors/0.2.1: - resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + performance-now@2.1.0: {} - /picocolors/1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@0.2.1: {} - /picomatch/2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + picocolors@1.1.1: {} - /pidtree/0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - dev: true + picomatch@2.3.1: {} - /pify/2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} + pidtree@0.6.0: {} - /pify/3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - dev: true + pify@2.3.0: {} - /pirates/4.0.5: - resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} - engines: {node: '>= 6'} + pirates@4.0.6: {} - /pkg-dir/4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 - /pkg-up/3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} + pkg-up@3.1.0: dependencies: find-up: 3.0.0 - /postcss-attribute-case-insensitive/5.0.2_postcss@8.4.16: - resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + pngjs@5.0.0: {} + + possible-typed-array-names@1.0.0: {} + + postcss-attribute-case-insensitive@5.0.2(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-browser-comments/4.0.0_yroec54rl3ndwvbunmnefp5nvy: - resolution: {integrity: sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==} - engines: {node: '>=8'} - peerDependencies: - browserslist: '>=4' - postcss: '>=8' + postcss-browser-comments@4.0.0(browserslist@4.24.2)(postcss@8.4.49): dependencies: - browserslist: 4.21.4 - postcss: 8.4.16 + browserslist: 4.24.2 + postcss: 8.4.49 - /postcss-calc/8.2.4_postcss@8.4.16: - resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} - peerDependencies: - postcss: ^8.2.2 + postcss-calc@8.2.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 - /postcss-clamp/4.1.0_postcss@8.4.16: - resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} - engines: {node: '>=7.6.0'} - peerDependencies: - postcss: ^8.4.6 + postcss-clamp@4.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-color-functional-notation/4.2.4_postcss@8.4.16: - resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-color-functional-notation@4.2.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-color-hex-alpha/8.0.4_postcss@8.4.16: - resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.4 + postcss-color-hex-alpha@8.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-color-rebeccapurple/7.1.1_postcss@8.4.16: - resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-color-rebeccapurple@7.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-colormin/5.3.0_postcss@8.4.16: - resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-colormin@5.3.1(postcss@8.4.49): dependencies: - browserslist: 4.21.4 + browserslist: 4.24.2 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-convert-values/5.1.2_postcss@8.4.16: - resolution: {integrity: sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-convert-values@5.1.3(postcss@8.4.49): dependencies: - browserslist: 4.21.4 - postcss: 8.4.16 + browserslist: 4.24.2 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-custom-media/8.0.2_postcss@8.4.16: - resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.3 + postcss-custom-media@8.0.2(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-custom-properties/12.1.9_postcss@8.4.16: - resolution: {integrity: sha512-/E7PRvK8DAVljBbeWrcEQJPG72jaImxF3vvCNFwv9cC8CzigVoNIpeyfnJzphnN3Fd8/auBf5wvkw6W9MfmTyg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-custom-properties@12.1.11(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-custom-selectors/6.0.3_postcss@8.4.16: - resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.3 + postcss-custom-selectors@6.0.3(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-dir-pseudo-class/6.0.5_postcss@8.4.16: - resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-dir-pseudo-class@6.0.5(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-discard-comments/5.1.2_postcss@8.4.16: - resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-discard-comments@5.1.2(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-discard-duplicates/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-discard-duplicates@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-discard-empty/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-discard-empty@5.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-discard-overridden/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-discard-overridden@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-double-position-gradients/3.1.2_postcss@8.4.16: - resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-double-position-gradients@3.1.2(postcss@8.4.49): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - postcss: 8.4.16 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-env-function/4.0.6_postcss@8.4.16: - resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.4 + postcss-env-function@4.0.6(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-flexbugs-fixes/5.0.2_postcss@8.4.16: - resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} - peerDependencies: - postcss: ^8.1.4 + postcss-flexbugs-fixes@5.0.2(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-focus-visible/6.0.4_postcss@8.4.16: - resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.4 + postcss-focus-visible@6.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-focus-within/5.0.4_postcss@8.4.16: - resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.4 + postcss-focus-within@5.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-font-variant/5.0.0_postcss@8.4.16: - resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} - peerDependencies: - postcss: ^8.1.0 + postcss-font-variant@5.0.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-gap-properties/3.0.5_postcss@8.4.16: - resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-gap-properties@3.0.5(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-image-set-function/4.0.7_postcss@8.4.16: - resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-image-set-function@4.0.7(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-import/14.1.0_postcss@8.4.16: - resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} - engines: {node: '>=10.0.0'} - peerDependencies: - postcss: ^8.0.0 + postcss-import@15.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.1 + resolve: 1.22.8 - /postcss-initial/4.0.1_postcss@8.4.16: - resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} - peerDependencies: - postcss: ^8.0.0 + postcss-initial@4.0.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-js/4.0.0_postcss@8.4.16: - resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.3.3 + postcss-js@4.0.1(postcss@8.4.49): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-lab-function/4.2.1_postcss@8.4.16: - resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-lab-function@4.2.1(postcss@8.4.49): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - postcss: 8.4.16 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-load-config/3.1.4_postcss@8.4.16: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true + postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)): dependencies: - lilconfig: 2.0.6 - postcss: 8.4.16 - yaml: 1.10.2 + lilconfig: 3.1.3 + yaml: 2.7.0 + optionalDependencies: + postcss: 8.4.49 + ts-node: 10.9.2(@types/node@20.5.1)(typescript@4.9.5) - /postcss-loader/6.2.1_qjv4cptcpse3y5hrjkrbb7drda: - resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} - engines: {node: '>= 12.13.0'} - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 + postcss-loader@6.2.1(postcss@8.4.49)(webpack@5.97.1): dependencies: - cosmiconfig: 7.0.1 - klona: 2.0.5 - postcss: 8.4.16 - semver: 7.3.7 - webpack: 5.74.0 + cosmiconfig: 7.1.0 + klona: 2.0.6 + postcss: 8.4.49 + semver: 7.6.3 + webpack: 5.97.1 - /postcss-logical/5.0.4_postcss@8.4.16: - resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.4 + postcss-logical@5.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-media-minmax/5.0.0_postcss@8.4.16: - resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - postcss: ^8.1.0 + postcss-media-minmax@5.0.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-merge-longhand/5.1.6_postcss@8.4.16: - resolution: {integrity: sha512-6C/UGF/3T5OE2CEbOuX7iNO63dnvqhGZeUnKkDeifebY0XqkkvrctYSZurpNE902LDf2yKwwPFgotnfSoPhQiw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-merge-longhand@5.1.7(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - stylehacks: 5.1.0_postcss@8.4.16 + stylehacks: 5.1.1(postcss@8.4.49) - /postcss-merge-rules/5.1.2_postcss@8.4.16: - resolution: {integrity: sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-merge-rules@5.1.4(postcss@8.4.49): dependencies: - browserslist: 4.21.4 + browserslist: 4.24.2 caniuse-api: 3.0.0 - cssnano-utils: 3.1.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + cssnano-utils: 3.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-minify-font-values/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-minify-font-values@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-minify-gradients/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-minify-gradients@5.1.1(postcss@8.4.49): dependencies: colord: 2.9.3 - cssnano-utils: 3.1.0_postcss@8.4.16 - postcss: 8.4.16 + cssnano-utils: 3.1.0(postcss@8.4.49) + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-minify-params/5.1.3_postcss@8.4.16: - resolution: {integrity: sha512-bkzpWcjykkqIujNL+EVEPOlLYi/eZ050oImVtHU7b4lFS82jPnsCb44gvC6pxaNt38Els3jWYDHTjHKf0koTgg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-minify-params@5.1.4(postcss@8.4.49): dependencies: - browserslist: 4.21.4 - cssnano-utils: 3.1.0_postcss@8.4.16 - postcss: 8.4.16 + browserslist: 4.24.2 + cssnano-utils: 3.1.0(postcss@8.4.49) + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-minify-selectors/5.2.1_postcss@8.4.16: - resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-minify-selectors@5.2.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-modules-extract-imports/3.0.0_postcss@8.4.16: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + postcss-modules-extract-imports@3.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-modules-local-by-default/4.0.0_postcss@8.4.16: - resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + postcss-modules-local-by-default@4.1.0(postcss@8.4.49): dependencies: - icss-utils: 5.1.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 - /postcss-modules-scope/3.0.0_postcss@8.4.16: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + postcss-modules-scope@3.2.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 7.0.0 - /postcss-modules-values/4.0.0_postcss@8.4.16: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 + postcss-modules-values@4.0.0(postcss@8.4.49): dependencies: - icss-utils: 5.1.0_postcss@8.4.16 - postcss: 8.4.16 + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 - /postcss-nested/5.0.6_postcss@8.4.16: - resolution: {integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 + postcss-nested@6.2.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-nesting/10.2.0_postcss@8.4.16: - resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-nesting@10.2.0(postcss@8.4.49): dependencies: - '@csstools/selector-specificity': 2.0.2_pnx64jze6bptzcedy5bidi3zdi - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-normalize-charset/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-charset@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-normalize-display-values/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-display-values@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-positions/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-positions@5.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-repeat-style/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-repeat-style@5.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-string/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-string@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-timing-functions/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-timing-functions@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-unicode/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-unicode@5.1.1(postcss@8.4.49): dependencies: - browserslist: 4.21.4 - postcss: 8.4.16 + browserslist: 4.24.2 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-url/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-url@5.1.0(postcss@8.4.49): dependencies: normalize-url: 6.1.0 - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize-whitespace/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-normalize-whitespace@5.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-normalize/10.0.1_yroec54rl3ndwvbunmnefp5nvy: - resolution: {integrity: sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==} - engines: {node: '>= 12'} - peerDependencies: - browserslist: '>= 4' - postcss: '>= 8' + postcss-normalize@10.0.1(browserslist@4.24.2)(postcss@8.4.49): dependencies: - '@csstools/normalize.css': 12.0.0 - browserslist: 4.21.4 - postcss: 8.4.16 - postcss-browser-comments: 4.0.0_yroec54rl3ndwvbunmnefp5nvy + '@csstools/normalize.css': 12.1.1 + browserslist: 4.24.2 + postcss: 8.4.49 + postcss-browser-comments: 4.0.0(browserslist@4.24.2)(postcss@8.4.49) sanitize.css: 13.0.0 - /postcss-opacity-percentage/1.1.2: - resolution: {integrity: sha512-lyUfF7miG+yewZ8EAk9XUBIlrHyUE6fijnesuz+Mj5zrIHIEw6KcIZSOk/elVMqzLvREmXB83Zi/5QpNRYd47w==} - engines: {node: ^12 || ^14 || >=16} - - /postcss-ordered-values/5.1.3_postcss@8.4.16: - resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-opacity-percentage@1.1.3(postcss@8.4.49): dependencies: - cssnano-utils: 3.1.0_postcss@8.4.16 - postcss: 8.4.16 - postcss-value-parser: 4.2.0 + postcss: 8.4.49 - /postcss-overflow-shorthand/3.0.4_postcss@8.4.16: - resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-ordered-values@5.1.3(postcss@8.4.49): dependencies: - postcss: 8.4.16 + cssnano-utils: 3.1.0(postcss@8.4.49) + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-page-break/3.0.4_postcss@8.4.16: - resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} - peerDependencies: - postcss: ^8 + postcss-overflow-shorthand@3.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 - /postcss-place/7.0.5_postcss@8.4.16: - resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-page-break@3.0.4(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-value-parser: 4.2.0 + postcss: 8.4.49 - /postcss-preset-env/7.8.2_postcss@8.4.16: - resolution: {integrity: sha512-rSMUEaOCnovKnwc5LvBDHUDzpGP+nrUeWZGWt9M72fBvckCi45JmnJigUr4QG4zZeOHmOCNCZnd2LKDvP++ZuQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-place@7.0.5(postcss@8.4.49): dependencies: - '@csstools/postcss-cascade-layers': 1.1.1_postcss@8.4.16 - '@csstools/postcss-color-function': 1.1.1_postcss@8.4.16 - '@csstools/postcss-font-format-keywords': 1.0.1_postcss@8.4.16 - '@csstools/postcss-hwb-function': 1.0.2_postcss@8.4.16 - '@csstools/postcss-ic-unit': 1.0.1_postcss@8.4.16 - '@csstools/postcss-is-pseudo-class': 2.0.7_postcss@8.4.16 - '@csstools/postcss-nested-calc': 1.0.0_postcss@8.4.16 - '@csstools/postcss-normalize-display-values': 1.0.1_postcss@8.4.16 - '@csstools/postcss-oklab-function': 1.1.1_postcss@8.4.16 - '@csstools/postcss-progressive-custom-properties': 1.3.0_postcss@8.4.16 - '@csstools/postcss-stepped-value-functions': 1.0.1_postcss@8.4.16 - '@csstools/postcss-text-decoration-shorthand': 1.0.0_postcss@8.4.16 - '@csstools/postcss-trigonometric-functions': 1.0.2_postcss@8.4.16 - '@csstools/postcss-unset-value': 1.0.2_postcss@8.4.16 - autoprefixer: 10.4.12_postcss@8.4.16 - browserslist: 4.21.4 - css-blank-pseudo: 3.0.3_postcss@8.4.16 - css-has-pseudo: 3.0.4_postcss@8.4.16 - css-prefers-color-scheme: 6.0.3_postcss@8.4.16 - cssdb: 7.0.1 - postcss: 8.4.16 - postcss-attribute-case-insensitive: 5.0.2_postcss@8.4.16 - postcss-clamp: 4.1.0_postcss@8.4.16 - postcss-color-functional-notation: 4.2.4_postcss@8.4.16 - postcss-color-hex-alpha: 8.0.4_postcss@8.4.16 - postcss-color-rebeccapurple: 7.1.1_postcss@8.4.16 - postcss-custom-media: 8.0.2_postcss@8.4.16 - postcss-custom-properties: 12.1.9_postcss@8.4.16 - postcss-custom-selectors: 6.0.3_postcss@8.4.16 - postcss-dir-pseudo-class: 6.0.5_postcss@8.4.16 - postcss-double-position-gradients: 3.1.2_postcss@8.4.16 - postcss-env-function: 4.0.6_postcss@8.4.16 - postcss-focus-visible: 6.0.4_postcss@8.4.16 - postcss-focus-within: 5.0.4_postcss@8.4.16 - postcss-font-variant: 5.0.0_postcss@8.4.16 - postcss-gap-properties: 3.0.5_postcss@8.4.16 - postcss-image-set-function: 4.0.7_postcss@8.4.16 - postcss-initial: 4.0.1_postcss@8.4.16 - postcss-lab-function: 4.2.1_postcss@8.4.16 - postcss-logical: 5.0.4_postcss@8.4.16 - postcss-media-minmax: 5.0.0_postcss@8.4.16 - postcss-nesting: 10.2.0_postcss@8.4.16 - postcss-opacity-percentage: 1.1.2 - postcss-overflow-shorthand: 3.0.4_postcss@8.4.16 - postcss-page-break: 3.0.4_postcss@8.4.16 - postcss-place: 7.0.5_postcss@8.4.16 - postcss-pseudo-class-any-link: 7.1.6_postcss@8.4.16 - postcss-replace-overflow-wrap: 4.0.0_postcss@8.4.16 - postcss-selector-not: 6.0.1_postcss@8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - - /postcss-pseudo-class-any-link/7.1.6_postcss@8.4.16: - resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + + postcss-preset-env@7.8.3(postcss@8.4.49): + dependencies: + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.4.49) + '@csstools/postcss-color-function': 1.1.1(postcss@8.4.49) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.4.49) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.4.49) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.4.49) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.4.49) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.4.49) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.4.49) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.4.49) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.49) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.4.49) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.4.49) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.4.49) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.4.49) + autoprefixer: 10.4.20(postcss@8.4.49) + browserslist: 4.24.2 + css-blank-pseudo: 3.0.3(postcss@8.4.49) + css-has-pseudo: 3.0.4(postcss@8.4.49) + css-prefers-color-scheme: 6.0.3(postcss@8.4.49) + cssdb: 7.11.2 + postcss: 8.4.49 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.4.49) + postcss-clamp: 4.1.0(postcss@8.4.49) + postcss-color-functional-notation: 4.2.4(postcss@8.4.49) + postcss-color-hex-alpha: 8.0.4(postcss@8.4.49) + postcss-color-rebeccapurple: 7.1.1(postcss@8.4.49) + postcss-custom-media: 8.0.2(postcss@8.4.49) + postcss-custom-properties: 12.1.11(postcss@8.4.49) + postcss-custom-selectors: 6.0.3(postcss@8.4.49) + postcss-dir-pseudo-class: 6.0.5(postcss@8.4.49) + postcss-double-position-gradients: 3.1.2(postcss@8.4.49) + postcss-env-function: 4.0.6(postcss@8.4.49) + postcss-focus-visible: 6.0.4(postcss@8.4.49) + postcss-focus-within: 5.0.4(postcss@8.4.49) + postcss-font-variant: 5.0.0(postcss@8.4.49) + postcss-gap-properties: 3.0.5(postcss@8.4.49) + postcss-image-set-function: 4.0.7(postcss@8.4.49) + postcss-initial: 4.0.1(postcss@8.4.49) + postcss-lab-function: 4.2.1(postcss@8.4.49) + postcss-logical: 5.0.4(postcss@8.4.49) + postcss-media-minmax: 5.0.0(postcss@8.4.49) + postcss-nesting: 10.2.0(postcss@8.4.49) + postcss-opacity-percentage: 1.1.3(postcss@8.4.49) + postcss-overflow-shorthand: 3.0.4(postcss@8.4.49) + postcss-page-break: 3.0.4(postcss@8.4.49) + postcss-place: 7.0.5(postcss@8.4.49) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.4.49) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.49) + postcss-selector-not: 6.0.1(postcss@8.4.49) + postcss-value-parser: 4.2.0 + + postcss-pseudo-class-any-link@7.1.6(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-reduce-initial/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-reduce-initial@5.1.2(postcss@8.4.49): dependencies: - browserslist: 4.21.4 + browserslist: 4.24.2 caniuse-api: 3.0.0 - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-reduce-transforms/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-reduce-transforms@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 - /postcss-replace-overflow-wrap/4.0.0_postcss@8.4.16: - resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} - peerDependencies: - postcss: ^8.0.3 + postcss-replace-overflow-wrap@4.0.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 - /postcss-selector-not/6.0.1_postcss@8.4.16: - resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} - engines: {node: ^12 || ^14 || >=16} - peerDependencies: - postcss: ^8.2 + postcss-selector-not@6.0.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-selector-parser/6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - /postcss-svgo/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-selector-parser@7.0.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@5.1.0(postcss@8.4.49): dependencies: - postcss: 8.4.16 + postcss: 8.4.49 postcss-value-parser: 4.2.0 svgo: 2.8.0 - /postcss-unique-selectors/5.1.1_postcss@8.4.16: - resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + postcss-unique-selectors@5.1.1(postcss@8.4.49): dependencies: - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /postcss-value-parser/4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss-value-parser@4.2.0: {} - /postcss/7.0.39: - resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} - engines: {node: '>=6.0.0'} + postcss@7.0.39: dependencies: picocolors: 0.2.1 source-map: 0.6.1 - /postcss/8.4.16: - resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==} - engines: {node: ^10 || ^12 || >=14} + postcss@8.4.49: dependencies: - nanoid: 3.3.4 - picocolors: 1.0.0 - source-map-js: 1.0.2 + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /prelude-ls/1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} + prelude-ls@1.1.2: {} - /prelude-ls/1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} + prelude-ls@1.2.1: {} - /prettier-linter-helpers/1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + prettier-linter-helpers@1.0.0: dependencies: - fast-diff: 1.2.0 - dev: true + fast-diff: 1.3.0 - /prettier/2.7.1: - resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true + prettier@3.4.2: {} - /pretty-bytes/5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} + pretty-bytes@5.6.0: {} - /pretty-error/4.0.0: - resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + pretty-error@4.0.0: dependencies: lodash: 4.17.21 renderkid: 3.0.0 - /pretty-format/24.9.0: - resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} - engines: {node: '>= 6'} + pretty-format@24.9.0: dependencies: '@jest/types': 24.9.0 ansi-regex: 4.1.1 ansi-styles: 3.2.1 react-is: 16.13.1 - dev: false - /pretty-format/27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 - /pretty-format/28.1.3: - resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} - engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + pretty-format@28.1.3: dependencies: '@jest/schemas': 28.1.3 ansi-regex: 5.0.1 ansi-styles: 5.2.0 - react-is: 18.2.0 + react-is: 18.3.1 - /process-nextick-args/2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-nextick-args@2.0.1: {} - /promise/8.2.0: - resolution: {integrity: sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==} + promise@8.3.0: dependencies: asap: 2.0.6 - /prompts/2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 - /prop-types-extra/1.1.1_react@18.2.0: - resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} - peerDependencies: - react: '>=0.14.0' + prop-types-extra@1.1.1(react@18.3.1): dependencies: - react: 18.2.0 + react: 18.3.1 react-is: 16.13.1 warning: 4.0.3 - dev: false - /prop-types/15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - /proxy-addr/2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - /psl/1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + proxy-from-env@1.1.0: {} - /punycode/2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} + psl@1.15.0: + dependencies: + punycode: 2.3.1 - /purgecss-webpack-plugin/4.1.3: - resolution: {integrity: sha512-1OHS0WE935w66FjaFSlV06ycmn3/A8a6Q+iVUmmCYAujQ1HPdX+psMXUhASEW0uF1PYEpOlhMc5ApigVqYK08g==} + punycode@2.3.1: {} + + purgecss-webpack-plugin@4.1.3(webpack@5.97.1): dependencies: purgecss: 4.1.3 - webpack: 5.74.0 + webpack: 5.97.1 webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - dev: true - /purgecss/4.1.3: - resolution: {integrity: sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw==} - hasBin: true + purgecss@4.1.3: dependencies: commander: 8.3.0 glob: 7.2.3 - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 - dev: true + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /q/1.5.1: - resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} - engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + q@1.5.1: {} - /qs/6.10.3: - resolution: {integrity: sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==} - engines: {node: '>=0.6'} + qrcode@1.5.4: dependencies: - side-channel: 1.0.4 + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 - /qs/6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} + qs@6.13.0: dependencies: - side-channel: 1.0.4 - dev: false + side-channel: 1.0.6 - /querystringify/2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + qs@6.13.1: + dependencies: + side-channel: 1.0.6 - /queue-microtask/1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + querystringify@2.2.0: {} - /quick-lru/4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - dev: true + queue-microtask@1.2.3: {} - /quick-lru/5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} + quick-lru@4.0.1: {} - /raf/3.4.1: - resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + raf@3.4.1: dependencies: performance-now: 2.1.0 - /randombytes/2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 - /range-parser/1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + range-parser@1.2.1: {} - /raw-body/2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} + raw-body@2.5.2: dependencies: bytes: 3.1.2 http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - /react-app-polyfill/3.0.0: - resolution: {integrity: sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==} - engines: {node: '>=14'} + react-app-polyfill@3.0.0: dependencies: - core-js: 3.25.2 + core-js: 3.39.0 object-assign: 4.1.1 - promise: 8.2.0 + promise: 8.3.0 raf: 3.4.1 - regenerator-runtime: 0.13.9 - whatwg-fetch: 3.6.2 + regenerator-runtime: 0.13.11 + whatwg-fetch: 3.6.20 - /react-app-rewired/2.2.1_react-scripts@5.0.1: - resolution: {integrity: sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==} - hasBin: true - peerDependencies: - react-scripts: '>=2.1.3' + react-app-rewired@2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@types/babel__core@7.20.5)(eslint@8.57.1)(react@18.3.1)(sass@1.54.4)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))(type-fest@1.4.0)(typescript@4.9.5)): dependencies: - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i - semver: 5.7.1 - dev: true + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@types/babel__core@7.20.5)(eslint@8.57.1)(react@18.3.1)(sass@1.54.4)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))(type-fest@1.4.0)(typescript@4.9.5) + semver: 5.7.2 - /react-bootstrap/2.5.0_7ey2zzynotv32rpkwno45fsx4e: - resolution: {integrity: sha512-j/aLR+okzbYk61TM3eDOU1NqOqnUdwyVrF+ojoCRUxPdzc2R0xXvqyRsjSoyRoCo7n82Fs/LWjPCin/QJNdwvA==} - peerDependencies: - '@types/react': '>=16.14.8' - react: '>=16.14.0' - react-dom: '>=16.14.0' - peerDependenciesMeta: - '@types/react': - optional: true + react-bootstrap@2.10.6(@types/react@18.3.16)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.19.0 - '@restart/hooks': 0.4.7_react@18.2.0 - '@restart/ui': 1.4.0_biqbaboplfbrettd7655fr4n2y - '@types/react': 18.0.20 - '@types/react-transition-group': 4.4.5 - classnames: 2.3.2 + '@babel/runtime': 7.26.0 + '@restart/hooks': 0.4.16(react@18.3.1) + '@restart/ui': 1.9.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react-transition-group': 4.4.11 + classnames: 2.5.1 dom-helpers: 5.2.1 invariant: 2.2.4 prop-types: 15.8.1 - prop-types-extra: 1.1.1_react@18.2.0 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y - uncontrollable: 7.2.1_react@18.2.0 + prop-types-extra: 1.1.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + uncontrollable: 7.2.1(react@18.3.1) warning: 4.0.3 - dev: false + optionalDependencies: + '@types/react': 18.3.16 - /react-dev-utils/12.0.1_npfwkgbcmgrbevrxnqgustqabe: - resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} - engines: {node: '>=14'} + react-dev-utils@12.0.1(eslint@8.57.1)(typescript@4.9.5)(webpack@5.97.1): dependencies: - '@babel/code-frame': 7.18.6 - address: 1.2.1 - browserslist: 4.21.4 + '@babel/code-frame': 7.26.2 + address: 1.2.2 + browserslist: 4.24.2 chalk: 4.1.2 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 detect-port-alt: 1.1.6 escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2_npfwkgbcmgrbevrxnqgustqabe + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.1)(typescript@4.9.5)(webpack@5.97.1) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 - immer: 9.0.15 + immer: 9.0.21 is-root: 2.1.0 - loader-utils: 3.2.0 - open: 8.4.0 + loader-utils: 3.3.1 + open: 8.4.2 pkg-up: 3.1.0 prompts: 2.4.2 react-error-overlay: 6.0.11 - recursive-readdir: 2.2.2 - shell-quote: 1.7.3 + recursive-readdir: 2.2.3 + shell-quote: 1.8.2 strip-ansi: 6.0.1 text-table: 0.2.0 - typescript: 4.8.3 - webpack: 5.74.0 + webpack: 5.97.1 + optionalDependencies: + typescript: 4.9.5 transitivePeerDependencies: - eslint - supports-color - - typescript - vue-template-compiler - - webpack - /react-dom/18.2.0_react@18.2.0: - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 + react: 18.3.1 + scheduler: 0.23.2 - /react-error-overlay/6.0.11: - resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + react-error-overlay@6.0.11: {} - /react-fast-compare/3.2.0: - resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} - dev: false + react-fast-compare@3.2.2: {} - /react-helmet-async/1.3.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} - peerDependencies: - react: ^16.6.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.26.0 invariant: 2.2.4 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-fast-compare: 3.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 shallowequal: 1.1.0 - dev: false - /react-i18next/11.18.6_ulhmqqxshznzmtuvahdi5nasbq: - resolution: {integrity: sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==} - peerDependencies: - i18next: '>= 19.0.0' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true + react-i18next@11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.26.0 html-parse-stringify: 3.0.1 - i18next: 21.9.2 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false + i18next: 21.10.0 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) - /react-is/16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@16.13.1: {} - /react-is/17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@17.0.2: {} - /react-is/18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@18.3.1: {} - /react-lifecycles-compat/3.0.4: - resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - dev: false + react-lifecycles-compat@3.0.4: {} - /react-refresh/0.11.0: - resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} - engines: {node: '>=0.10.0'} + react-refresh@0.11.0: {} - /react-router-dom/6.4.0_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-4Aw1xmXKeleYYQ3x0Lcl2undHR6yMjXZjd9DKZd53SGOYqirrUThyUb0wwAX5VZAyvSuzjNJmZlJ3rR9+/vzqg==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react-router-dom@7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - react-router: 6.4.0_react@18.2.0 - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - /react-router/6.4.0_react@18.2.0: - resolution: {integrity: sha512-B+5bEXFlgR1XUdHYR6P94g299SjrfCBMmEDJNcFbpAyRH1j1748yt9NdDhW3++nw1lk3zQJ6aOO66zUx3KlTZg==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' + react-router@7.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.0.0 - react: 18.2.0 - dev: false + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) - /react-scripts/5.0.1_r727nmttzgvwuocpb6eyxi2m5i: - resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} - engines: {node: '>=14.0.0'} - hasBin: true - peerDependencies: - react: '>= 16' - typescript: ^3.2.1 || ^4 - peerDependenciesMeta: - typescript: - optional: true + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(@types/babel__core@7.20.5)(eslint@8.57.1)(react@18.3.1)(sass@1.54.4)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))(type-fest@1.4.0)(typescript@4.9.5): dependencies: - '@babel/core': 7.19.1 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.7_prxwy2zxcolvdag5hfkyuqbcze + '@babel/core': 7.26.0 + '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(react-refresh@0.11.0)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.97.1))(webpack@5.97.1) '@svgr/webpack': 5.5.0 - babel-jest: 27.5.1_@babel+core@7.19.1 - babel-loader: 8.2.5_rhsdbzevgb5tizdhlla5jsbgyu - babel-plugin-named-asset-import: 0.3.8_@babel+core@7.19.1 + babel-jest: 27.5.1(@babel/core@7.26.0) + babel-loader: 8.4.1(@babel/core@7.26.0)(webpack@5.97.1) + babel-plugin-named-asset-import: 0.3.8(@babel/core@7.26.0) babel-preset-react-app: 10.0.1 - bfj: 7.0.2 - browserslist: 4.21.4 + bfj: 7.1.0 + browserslist: 4.24.2 camelcase: 6.3.0 case-sensitive-paths-webpack-plugin: 2.4.0 - css-loader: 6.7.1_webpack@5.74.0 - css-minimizer-webpack-plugin: 3.4.1_webpack@5.74.0 + css-loader: 6.11.0(webpack@5.97.1) + css-minimizer-webpack-plugin: 3.4.1(webpack@5.97.1) dotenv: 10.0.0 dotenv-expand: 5.1.0 - eslint: 8.23.1 - eslint-config-react-app: 7.0.1_ep5hkfurrjf46kbnkcej3benz4 - eslint-webpack-plugin: 3.2.0_cnsurwdbw57xgwxuf5k544xt5e - file-loader: 6.2.0_webpack@5.74.0 + eslint: 8.57.1 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.0))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0))(eslint@8.57.1)(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)))(typescript@4.9.5) + eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.97.1) + file-loader: 6.2.0(webpack@5.97.1) fs-extra: 10.1.0 - html-webpack-plugin: 5.5.0_webpack@5.74.0 + html-webpack-plugin: 5.6.3(webpack@5.97.1) identity-obj-proxy: 3.0.0 - jest: 27.5.1 + jest: 27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) jest-resolve: 27.5.1 - jest-watch-typeahead: 1.1.0_jest@27.5.1 - mini-css-extract-plugin: 2.6.1_webpack@5.74.0 - postcss: 8.4.16 - postcss-flexbugs-fixes: 5.0.2_postcss@8.4.16 - postcss-loader: 6.2.1_qjv4cptcpse3y5hrjkrbb7drda - postcss-normalize: 10.0.1_yroec54rl3ndwvbunmnefp5nvy - postcss-preset-env: 7.8.2_postcss@8.4.16 + jest-watch-typeahead: 1.1.0(jest@27.5.1(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5))) + mini-css-extract-plugin: 2.9.2(webpack@5.97.1) + postcss: 8.4.49 + postcss-flexbugs-fixes: 5.0.2(postcss@8.4.49) + postcss-loader: 6.2.1(postcss@8.4.49)(webpack@5.97.1) + postcss-normalize: 10.0.1(browserslist@4.24.2)(postcss@8.4.49) + postcss-preset-env: 7.8.3(postcss@8.4.49) prompts: 2.4.2 - react: 18.2.0 + react: 18.3.1 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1_npfwkgbcmgrbevrxnqgustqabe + react-dev-utils: 12.0.1(eslint@8.57.1)(typescript@4.9.5)(webpack@5.97.1) react-refresh: 0.11.0 - resolve: 1.22.1 + resolve: 1.22.8 resolve-url-loader: 4.0.0 - sass-loader: 12.6.0_sass@1.54.9+webpack@5.74.0 - semver: 7.3.7 - source-map-loader: 3.0.1_webpack@5.74.0 - style-loader: 3.3.1_webpack@5.74.0 - tailwindcss: 3.1.8 - terser-webpack-plugin: 5.3.6_webpack@5.74.0 - typescript: 4.8.3 - webpack: 5.74.0 - webpack-dev-server: 4.11.1_webpack@5.74.0 - webpack-manifest-plugin: 4.1.1_webpack@5.74.0 - workbox-webpack-plugin: 6.5.4_webpack@5.74.0 + sass-loader: 12.6.0(sass@1.54.4)(webpack@5.97.1) + semver: 7.6.3 + source-map-loader: 3.0.2(webpack@5.97.1) + style-loader: 3.3.4(webpack@5.97.1) + tailwindcss: 3.4.16(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) + terser-webpack-plugin: 5.3.10(webpack@5.97.1) + webpack: 5.97.1 + webpack-dev-server: 4.15.2(webpack@5.97.1) + webpack-manifest-plugin: 4.1.1(webpack@5.97.1) + workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.97.1) optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 + typescript: 4.9.5 transitivePeerDependencies: - '@babel/plugin-syntax-flow' - '@babel/plugin-transform-react-jsx' - '@parcel/css' + - '@rspack/core' - '@swc/core' - '@types/babel__core' - '@types/webpack' @@ -9750,69 +13723,37 @@ packages: - webpack-hot-middleware - webpack-plugin-serve - /react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} - peerDependencies: - react: '>=16.6.0' - react-dom: '>=16.6.0' + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.26.0 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react/18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + react@18.3.1: dependencies: loose-envify: 1.4.0 - /read-cache/1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + read-cache@1.0.0: dependencies: pify: 2.3.0 - /read-pkg-up/3.0.0: - resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} - engines: {node: '>=4'} - dependencies: - find-up: 2.1.0 - read-pkg: 3.0.0 - dev: true - - /read-pkg-up/7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 - dev: true - /read-pkg/3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} - dependencies: - load-json-file: 4.0.0 - normalize-package-data: 2.5.0 - path-type: 3.0.0 - dev: true - - /read-pkg/5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} + read-pkg@5.2.0: dependencies: - '@types/normalize-package-data': 2.4.1 + '@types/normalize-package-data': 2.4.4 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 - dev: true - /readable-stream/2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -9822,91 +13763,77 @@ packages: string_decoder: 1.1.1 util-deprecate: 1.0.2 - /readable-stream/3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - /readdirp/3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + readdirp@3.6.0: dependencies: picomatch: 2.3.1 - /recursive-readdir/2.2.2: - resolution: {integrity: sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==} - engines: {node: '>=0.10.0'} + recursive-readdir@2.2.3: dependencies: - minimatch: 3.0.4 + minimatch: 3.1.2 - /redent/3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - /regenerate-unicode-properties/10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} + reflect.getprototypeof@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + dunder-proto: 1.0.0 + es-abstract: 1.23.5 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + which-builtin-type: 1.2.0 + + regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 - /regenerate/1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} - /regenerator-runtime/0.13.9: - resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + regenerator-runtime@0.14.1: {} - /regenerator-transform/0.15.0: - resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} + regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.19.0 + '@babel/runtime': 7.26.0 - /regex-parser/2.2.11: - resolution: {integrity: sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==} + regex-parser@2.3.0: {} - /regexp.prototype.flags/1.4.3: - resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} - engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.3: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - functions-have-names: 1.2.3 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 - /regexpp/3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - /regexpu-core/5.2.1: - resolution: {integrity: sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==} - engines: {node: '>=4'} + regexpu-core@6.2.0: dependencies: regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsgen: 0.7.1 - regjsparser: 0.9.1 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 - /regjsgen/0.7.1: - resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} + regjsgen@0.8.0: {} - /regjsparser/0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true + regjsparser@0.12.0: dependencies: - jsesc: 0.5.0 + jsesc: 3.0.2 - /relateurl/0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} - engines: {node: '>= 0.10'} + relateurl@0.2.7: {} - /renderkid/3.0.0: - resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + renderkid@3.0.0: dependencies: css-select: 4.3.0 dom-converter: 0.2.0 @@ -9914,286 +13841,175 @@ packages: lodash: 4.17.21 strip-ansi: 6.0.1 - /require-directory/2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + require-directory@2.1.1: {} - /require-from-string/2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + require-from-string@2.0.2: {} - /requires-port/1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + require-main-filename@2.0.0: {} - /resize-observer-polyfill/1.5.1: - resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - dev: false + requires-port@1.0.0: {} - /resolve-cwd/3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 - /resolve-dir/1.0.1: - resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} - engines: {node: '>=0.10.0'} - dependencies: - expand-tilde: 2.0.2 - global-modules: 1.0.0 - dev: true - - /resolve-from/4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolve-from@4.0.0: {} - /resolve-from/5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + resolve-from@5.0.0: {} - /resolve-global/1.0.0: - resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} - engines: {node: '>=8'} + resolve-global@1.0.0: dependencies: global-dirs: 0.1.1 - dev: true - /resolve-url-loader/4.0.0: - resolution: {integrity: sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==} - engines: {node: '>=8.9'} - peerDependencies: - rework: 1.0.1 - rework-visit: 1.0.0 - peerDependenciesMeta: - rework: - optional: true - rework-visit: - optional: true + resolve-pkg-maps@1.0.0: {} + + resolve-url-loader@4.0.0: dependencies: adjust-sourcemap-loader: 4.0.0 - convert-source-map: 1.8.0 - loader-utils: 2.0.2 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 postcss: 7.0.39 source-map: 0.6.1 - /resolve-url/0.2.1: - resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} - deprecated: https://github.com/lydell/resolve-url#deprecated - dev: false + resolve-url@0.2.1: {} - /resolve.exports/1.1.0: - resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} - engines: {node: '>=10'} + resolve.exports@1.1.1: {} - /resolve/1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true + resolve@1.22.8: dependencies: - is-core-module: 2.10.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - /resolve/2.0.0-next.4: - resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} - hasBin: true + resolve@2.0.0-next.5: dependencies: - is-core-module: 2.10.0 + is-core-module: 2.15.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - /restore-cursor/3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@5.1.0: dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: true + onetime: 7.0.0 + signal-exit: 4.1.0 - /retry/0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} + retry@0.13.1: {} - /reusify/1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + reusify@1.0.4: {} - /rfdc/1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - dev: true + rfdc@1.4.1: {} - /rimraf/3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true + rimraf@2.6.3: dependencies: glob: 7.2.3 - /robust-predicates/3.0.1: - resolution: {integrity: sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==} - dev: false + rimraf@3.0.2: + dependencies: + glob: 7.2.3 - /rollup-plugin-terser/7.0.2_rollup@2.79.0: - resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} - peerDependencies: - rollup: ^2.0.0 + rollup-plugin-terser@7.0.2(rollup@2.79.2): dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.26.2 jest-worker: 26.6.2 - rollup: 2.79.0 + rollup: 2.79.2 serialize-javascript: 4.0.0 - terser: 5.15.0 + terser: 5.37.0 - /rollup/2.79.0: - resolution: {integrity: sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA==} - engines: {node: '>=10.0.0'} - hasBin: true + rollup@2.79.2: optionalDependencies: - fsevents: 2.3.2 - - /run-async/2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - dev: true + fsevents: 2.3.3 - /run-parallel/1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - /rw/1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - dev: false - - /rxjs/7.5.6: - resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==} + safe-array-concat@1.1.2: dependencies: - tslib: 2.4.0 - dev: true - - /safe-buffer/5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - /safe-buffer/5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - /safer-buffer/2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - /sanitize.css/13.0.0: - resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==} + call-bind: 1.0.8 + get-intrinsic: 1.2.5 + has-symbols: 1.1.0 + isarray: 2.0.5 - /sass-loader/12.6.0_sass@1.54.9+webpack@5.74.0: - resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} - engines: {node: '>= 12.13.0'} - peerDependencies: - fibers: '>= 3.1.0' - node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - sass: ^1.3.0 - sass-embedded: '*' - webpack: ^5.0.0 - peerDependenciesMeta: - fibers: - optional: true - node-sass: - optional: true - sass: - optional: true - sass-embedded: - optional: true + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + is-regex: 1.2.0 + + safer-buffer@2.1.2: {} + + sanitize.css@13.0.0: {} + + sass-loader@12.6.0(sass@1.54.4)(webpack@5.97.1): dependencies: - klona: 2.0.5 + klona: 2.0.6 neo-async: 2.6.2 - sass: 1.54.9 - webpack: 5.74.0 + webpack: 5.97.1 + optionalDependencies: + sass: 1.54.4 - /sass/1.54.9: - resolution: {integrity: sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==} - engines: {node: '>=12.0.0'} - hasBin: true + sass@1.54.4: dependencies: - chokidar: 3.5.3 - immutable: 4.1.0 - source-map-js: 1.0.2 + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.1 - /sax/1.2.4: - resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + sax@1.2.4: {} - /saxes/5.0.1: - resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} - engines: {node: '>=10'} + saxes@5.0.1: dependencies: xmlchars: 2.2.0 - /scheduler/0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 - /schema-utils/2.7.0: - resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} - engines: {node: '>= 8.9.0'} + schema-utils@2.7.0: dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.15 ajv: 6.12.6 - ajv-keywords: 3.5.2_ajv@6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) - /schema-utils/2.7.1: - resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} - engines: {node: '>= 8.9.0'} + schema-utils@2.7.1: dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.15 ajv: 6.12.6 - ajv-keywords: 3.5.2_ajv@6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) - /schema-utils/3.1.1: - resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} - engines: {node: '>= 10.13.0'} + schema-utils@3.3.0: dependencies: - '@types/json-schema': 7.0.11 + '@types/json-schema': 7.0.15 ajv: 6.12.6 - ajv-keywords: 3.5.2_ajv@6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) - /schema-utils/4.0.0: - resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==} - engines: {node: '>= 12.13.0'} + schema-utils@4.2.0: dependencies: - '@types/json-schema': 7.0.11 - ajv: 8.11.0 - ajv-formats: 2.1.1 - ajv-keywords: 5.1.0_ajv@8.11.0 - - /screenfull/5.2.0: - resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} - engines: {node: '>=0.10.0'} - dev: false + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) - /select-hose/2.0.0: - resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + select-hose@2.0.0: {} - /selfsigned/2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} - engines: {node: '>=10'} + selfsigned@2.4.1: dependencies: + '@types/node-forge': 1.3.11 node-forge: 1.3.1 - /semver/5.7.1: - resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} - hasBin: true - dev: true + semver@5.7.2: {} - /semver/6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true + semver@6.3.1: {} - /semver/7.3.7: - resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} - engines: {node: '>=10'} - hasBin: true + semver@7.5.4: dependencies: lru-cache: 6.0.0 - /send/0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} + semver@7.6.3: {} + + send@0.19.0: dependencies: debug: 2.6.9 depd: 2.0.0 @@ -10211,19 +14027,15 @@ packages: transitivePeerDependencies: - supports-color - /serialize-javascript/4.0.0: - resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + serialize-javascript@4.0.0: dependencies: randombytes: 2.1.0 - /serialize-javascript/6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 - /serve-index/1.9.1: - resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} - engines: {node: '>= 0.8.0'} + serve-index@1.9.1: dependencies: accepts: 1.3.8 batch: 0.6.1 @@ -10235,190 +14047,165 @@ packages: transitivePeerDependencies: - supports-color - /serve-static/1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} + serve-static@1.16.2: dependencies: - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.18.0 + send: 0.19.0 transitivePeerDependencies: - supports-color - /setprototypeof/1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + set-blocking@2.0.0: {} - /setprototypeof/1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + set-cookie-parser@2.7.1: {} - /shallowequal/1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - dev: false + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 - /shebang-command/2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + set-function-name@2.0.2: dependencies: - shebang-regex: 3.0.0 + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 - /shebang-regex/3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} - /shell-quote/1.7.3: - resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + shallowequal@1.1.0: {} - /side-channel/1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + shebang-command@2.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.1.3 - object-inspect: 1.12.2 + shebang-regex: 3.0.0 - /signal-exit/3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + shebang-regex@3.0.0: {} - /sisteransi/1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + shell-quote@1.8.2: {} - /slash/3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + side-channel@1.0.6: + dependencies: + call-bind: 1.0.8 + es-errors: 1.3.0 + get-intrinsic: 1.2.5 + object-inspect: 1.13.3 - /slash/4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} + signal-exit@3.0.7: {} - /slice-ansi/3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: true + signal-exit@4.1.0: {} - /slice-ansi/4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} + simple-swizzle@0.2.2: dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: true + is-arrayish: 0.3.2 - /slice-ansi/5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + slash@4.0.0: {} + + slice-ansi@5.0.0: dependencies: - ansi-styles: 6.1.1 + ansi-styles: 6.2.1 is-fullwidth-code-point: 4.0.0 - dev: true - /sockjs/0.3.24: - resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 uuid: 8.3.2 websocket-driver: 0.7.4 - /source-list-map/2.0.1: - resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + source-list-map@2.0.1: {} - /source-map-js/1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + source-map-explorer@2.5.3: + dependencies: + btoa: 1.2.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + ejs: 3.1.10 + escape-html: 1.0.3 + glob: 7.2.3 + gzip-size: 6.0.0 + lodash: 4.17.21 + open: 7.4.2 + source-map: 0.7.4 + temp: 0.9.4 + yargs: 16.2.0 - /source-map-loader/3.0.1_webpack@5.74.0: - resolution: {integrity: sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 + source-map-js@1.2.1: {} + + source-map-loader@3.0.2(webpack@5.97.1): dependencies: abab: 2.0.6 iconv-lite: 0.6.3 - source-map-js: 1.0.2 - webpack: 5.74.0 + source-map-js: 1.2.1 + webpack: 5.97.1 - /source-map-resolve/0.5.3: - resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated + source-map-resolve@0.5.3: dependencies: atob: 2.1.2 - decode-uri-component: 0.2.0 + decode-uri-component: 0.2.2 resolve-url: 0.2.1 source-map-url: 0.4.1 urix: 0.1.0 - dev: false - /source-map-support/0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - /source-map-url/0.4.1: - resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} - deprecated: See https://github.com/lydell/source-map-url#deprecated - dev: false + source-map-url@0.4.1: {} - /source-map/0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + source-map@0.6.1: {} - /source-map/0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} + source-map@0.7.4: {} - /source-map/0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 - /sourcemap-codec/1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + sourcemap-codec@1.4.8: {} - /spdx-correct/3.1.1: - resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.12 - dev: true + spdx-license-ids: 3.0.20 - /spdx-exceptions/2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true + spdx-exceptions@2.5.0: {} - /spdx-expression-parse/3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@3.0.1: dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.12 - dev: true + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.20 - /spdx-license-ids/3.0.12: - resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} - dev: true + spdx-license-ids@3.0.20: {} - /spdy-transport/3.0.0: - resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + spdy-transport@3.0.0: dependencies: - debug: 4.3.4 + debug: 4.4.0 detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 - readable-stream: 3.6.0 + readable-stream: 3.6.2 wbuf: 1.7.3 transitivePeerDependencies: - supports-color - /spdy/4.0.2: - resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} - engines: {node: '>=6.0.0'} + spdy@4.0.2: dependencies: - debug: 4.3.4 + debug: 4.4.0 handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -10426,227 +14213,191 @@ packages: transitivePeerDependencies: - supports-color - /split/1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} - dependencies: - through: 2.3.8 - dev: true - - /split2/3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@3.2.2: dependencies: - readable-stream: 3.6.0 - dev: true + readable-stream: 3.6.2 - /sprintf-js/1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.0.3: {} - /stable/0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + stable@0.1.8: {} - /stack-utils/2.0.5: - resolution: {integrity: sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==} - engines: {node: '>=10'} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 - /stackframe/1.3.4: - resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + stackframe@1.3.4: {} - /statuses/1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} + static-eval@2.0.2: + dependencies: + escodegen: 1.14.3 - /statuses/2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} + statuses@1.5.0: {} - /string-argv/0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - dev: true + statuses@2.0.1: {} - /string-length/4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + stop-iteration-iterator@1.0.0: + dependencies: + internal-slot: 1.0.7 + + string-argv@0.3.2: {} + + string-length@4.0.2: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 - /string-length/5.0.1: - resolution: {integrity: sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==} - engines: {node: '>=12.20'} + string-length@5.0.1: dependencies: - char-regex: 2.0.1 - strip-ansi: 7.0.1 + char-regex: 2.0.2 + strip-ansi: 7.1.0 - /string-natural-compare/3.0.1: - resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} + string-natural-compare@3.0.1: {} - /string-width/4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width/5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + string-width@5.1.2: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - dev: true + strip-ansi: 7.1.0 - /string.prototype.matchall/4.0.7: - resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} + string-width@7.2.0: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 - get-intrinsic: 1.1.3 - has-symbols: 1.0.3 - internal-slot: 1.0.3 - regexp.prototype.flags: 1.4.3 - side-channel: 1.0.4 + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 - /string.prototype.trimend/1.0.5: - resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + string.prototype.includes@2.0.1: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 - /string.prototype.trimstart/1.0.5: - resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + string.prototype.matchall@4.0.11: dependencies: - call-bind: 1.0.2 - define-properties: 1.1.4 - es-abstract: 1.20.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.5 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.3 + set-function-name: 2.0.2 + side-channel: 1.0.6 - /string_decoder/1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.5 + + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 - /string_decoder/1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - /stringify-object/3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} + stringify-object@3.3.0: dependencies: get-own-enumerable-property-symbols: 3.0.2 is-obj: 1.0.1 is-regexp: 1.0.0 - /strip-ansi/6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - /strip-ansi/7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} - engines: {node: '>=12'} + strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 + ansi-regex: 6.1.0 - /strip-bom/3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + strip-bom@3.0.0: {} - /strip-bom/4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} + strip-bom@4.0.0: {} - /strip-comments/2.0.1: - resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} - engines: {node: '>=10'} + strip-comments@2.0.1: {} - /strip-final-newline/2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} + strip-final-newline@2.0.0: {} - /strip-final-newline/3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + strip-final-newline@3.0.0: {} - /strip-indent/3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 - /strip-json-comments/3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} + strip-json-comments@3.1.1: {} - /style-loader/3.3.1_webpack@5.74.0: - resolution: {integrity: sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 + style-loader@3.3.4(webpack@5.97.1): dependencies: - webpack: 5.74.0 + webpack: 5.97.1 - /stylehacks/5.1.0_postcss@8.4.16: - resolution: {integrity: sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 + style-mod@4.1.2: {} + + stylehacks@5.1.1(postcss@8.4.49): dependencies: - browserslist: 4.21.4 - postcss: 8.4.16 - postcss-selector-parser: 6.0.10 + browserslist: 4.24.2 + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 - /stylis/4.1.2: - resolution: {integrity: sha512-Nn2CCrG2ZaFziDxaZPN43CXqn+j7tcdjPFCkRBkFue8QYXC2HdEwnw5TCBo4yQZ2WxKYeSi0fdoOrtEqgDrXbA==} - dev: false + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 - /supports-color/5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 - /supports-color/7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 - /supports-color/8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + supports-color@8.1.1: dependencies: has-flag: 4.0.0 - /supports-hyperlinks/2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} + supports-hyperlinks@2.3.0: dependencies: has-flag: 4.0.0 supports-color: 7.2.0 - /supports-preserve-symlinks-flag/1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + supports-preserve-symlinks-flag@1.0.0: {} - /svg-parser/2.0.4: - resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + svg-parser@2.0.4: {} - /svgo/1.3.2: - resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} - engines: {node: '>=4.0.0'} - deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. - hasBin: true + svgo@1.3.2: dependencies: chalk: 2.4.2 coa: 2.0.2 @@ -10656,1067 +14407,812 @@ packages: csso: 4.2.0 js-yaml: 3.14.1 mkdirp: 0.5.6 - object.values: 1.1.5 + object.values: 1.2.0 sax: 1.2.4 stable: 0.1.8 unquote: 1.1.1 util.promisify: 1.0.1 - /svgo/2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true + svgo@2.8.0: dependencies: '@trysound/sax': 0.2.0 commander: 7.2.0 css-select: 4.3.0 css-tree: 1.1.3 csso: 4.2.0 - picocolors: 1.0.0 + picocolors: 1.1.1 stable: 0.1.8 - /swr/1.3.0_react@18.2.0: - resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 + swr@1.3.0(react@18.3.1): dependencies: - react: 18.2.0 - dev: false + react: 18.3.1 - /symbol-tree/3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + symbol-tree@3.2.4: {} - /tailwindcss/3.1.8: - resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} - engines: {node: '>=12.13.0'} - hasBin: true + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + tailwindcss@3.4.16(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)): dependencies: + '@alloc/quick-lru': 5.2.0 arg: 5.0.2 - chokidar: 3.5.3 - color-name: 1.1.4 - detective: 5.2.1 + chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.2.12 + fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 - lilconfig: 2.0.6 + jiti: 1.21.6 + lilconfig: 3.1.3 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.0.0 - postcss: 8.4.16 - postcss-import: 14.1.0_postcss@8.4.16 - postcss-js: 4.0.0_postcss@8.4.16 - postcss-load-config: 3.1.4_postcss@8.4.16 - postcss-nested: 5.0.6_postcss@8.4.16 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - quick-lru: 5.1.1 - resolve: 1.22.1 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.0.1(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@16.18.121)(typescript@4.9.5)) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 transitivePeerDependencies: - ts-node - /tapable/1.1.3: - resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} - engines: {node: '>=6'} + tapable@1.1.3: {} - /tapable/2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} + tapable@2.2.1: {} - /temp-dir/2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} + temp-dir@2.0.0: {} - /tempfile/3.0.0: - resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} - engines: {node: '>=8'} + temp@0.9.4: dependencies: - temp-dir: 2.0.0 - uuid: 3.4.0 - dev: true + mkdirp: 0.5.6 + rimraf: 2.6.3 - /tempy/0.6.0: - resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} - engines: {node: '>=10'} + tempy@0.6.0: dependencies: is-stream: 2.0.1 temp-dir: 2.0.0 type-fest: 0.16.0 unique-string: 2.0.0 - /terminal-link/2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} + terminal-link@2.1.1: dependencies: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - /terser-webpack-plugin/5.3.6_webpack@5.74.0: - resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true + terser-webpack-plugin@5.3.10(webpack@5.97.1): dependencies: - '@jridgewell/trace-mapping': 0.3.15 + '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 - schema-utils: 3.1.1 - serialize-javascript: 6.0.0 - terser: 5.15.0 - webpack: 5.74.0 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.37.0 + webpack: 5.97.1 - /terser/5.15.0: - resolution: {integrity: sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==} - engines: {node: '>=10'} - hasBin: true + terser@5.37.0: dependencies: - '@jridgewell/source-map': 0.3.2 - acorn: 8.8.0 + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 commander: 2.20.3 source-map-support: 0.5.21 - /test-exclude/6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 - /text-extensions/1.9.0: - resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} - engines: {node: '>=0.10'} - dev: true - - /text-table/0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - /throat/6.0.1: - resolution: {integrity: sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==} + text-extensions@1.9.0: {} - /through/2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true + text-table@0.2.0: {} - /through2/2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + thenify-all@1.6.0: dependencies: - readable-stream: 2.3.7 - xtend: 4.0.2 - dev: true + thenify: 3.3.1 - /through2/4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + thenify@3.3.1: dependencies: - readable-stream: 3.6.0 - dev: true + any-promise: 1.3.0 - /thunky/1.1.0: - resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + throat@6.0.2: {} - /tmp/0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + through2@4.0.2: dependencies: - os-tmpdir: 1.0.2 - dev: true + readable-stream: 3.6.2 - /tmpl/1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + through@2.3.8: {} - /to-fast-properties/2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} + thunky@1.1.0: {} - /to-regex-range/5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + tmpl@1.0.5: {} + + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - /toggle-selection/1.0.6: - resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - dev: false + toggle-selection@1.0.6: {} - /toidentifier/1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + toidentifier@1.0.1: {} - /tough-cookie/4.1.2: - resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} - engines: {node: '>=6'} + tough-cookie@4.1.4: dependencies: - psl: 1.9.0 - punycode: 2.1.1 + psl: 1.15.0 + punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10 - /tr46/0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /tr46/1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@1.0.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 - /tr46/2.1.0: - resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} - engines: {node: '>=8'} + tr46@2.1.0: dependencies: - punycode: 2.1.1 - - /trim-newlines/3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - dev: true + punycode: 2.3.1 - /tryer/1.0.1: - resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + trim-newlines@3.0.1: {} - /ts-node/10.9.1_ck2axrxkiif44rdbzjywaqjysa: - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true + tryer@1.0.1: {} + + ts-api-utils@1.4.3(typescript@4.9.5): + dependencies: + typescript: 4.9.5 + + ts-interface-checker@0.1.13: {} + + ts-node@10.9.2(@types/node@20.5.1)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.9 + '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.3 - '@types/node': 14.18.29 - acorn: 8.8.0 - acorn-walk: 8.2.0 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.5.1 + acorn: 8.14.0 + acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.8.3 + typescript: 4.9.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - dev: true - - /tsconfig-paths-webpack-plugin/4.0.0: - resolution: {integrity: sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ==} - engines: {node: '>=10.13.0'} - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.10.0 - tsconfig-paths: 4.1.0 - dev: true - /tsconfig-paths/3.14.1: - resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 - json5: 1.0.1 - minimist: 1.2.6 + json5: 1.0.2 + minimist: 1.2.8 strip-bom: 3.0.0 - /tsconfig-paths/4.1.0: - resolution: {integrity: sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==} - engines: {node: '>=6'} - dependencies: - json5: 2.2.1 - minimist: 1.2.6 - strip-bom: 3.0.0 - dev: true - - /tslib/1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@1.14.1: {} - /tslib/2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + tslib@2.8.1: {} - /tsutils/3.21.0_typescript@4.8.3: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsutils@3.21.0(typescript@4.9.5): dependencies: tslib: 1.14.1 - typescript: 4.8.3 + typescript: 4.9.5 - /type-check/0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} + turbo-stream@2.4.0: {} + + type-check@0.3.2: dependencies: prelude-ls: 1.1.2 - /type-check/0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - /type-detect/4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + type-detect@4.0.8: {} - /type-fest/0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} + type-fest@0.16.0: {} - /type-fest/0.18.1: - resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} - engines: {node: '>=10'} - dev: true + type-fest@0.18.1: {} - /type-fest/0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} + type-fest@0.20.2: {} - /type-fest/0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} + type-fest@0.21.3: {} - /type-fest/0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true + type-fest@0.6.0: {} - /type-fest/0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true + type-fest@0.8.1: {} - /type-is/1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + type-fest@1.4.0: + optional: true + + type-is@1.6.18: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 - /typedarray-to-buffer/3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typed-array-buffer@1.0.2: dependencies: - is-typedarray: 1.0.0 + call-bind: 1.0.8 + es-errors: 1.3.0 + is-typed-array: 1.1.13 - /typescript/4.8.3: - resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==} - engines: {node: '>=4.2.0'} - hasBin: true + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.13 - /uglify-js/3.17.1: - resolution: {integrity: sha512-+juFBsLLw7AqMaqJ0GFvlsGZwdQfI2ooKQB39PSBgMnMakcFosi9O8jCwE+2/2nMNcc0z63r9mwjoDG8zr+q0Q==} - engines: {node: '>=0.8.0'} - hasBin: true - requiresBuild: true - dev: true - optional: true + typed-array-byte-offset@1.0.3: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.13 + reflect.getprototypeof: 1.0.8 - /unbox-primitive/1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + reflect.getprototypeof: 1.0.8 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@4.9.5: {} + + unbox-primitive@1.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.8 has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.0 - /uncontrollable/7.2.1_react@18.2.0: - resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} - peerDependencies: - react: '>=15.0.0' + uncontrollable@7.2.1(react@18.3.1): dependencies: - '@babel/runtime': 7.19.0 - '@types/react': 18.0.20 + '@babel/runtime': 7.26.0 + '@types/react': 18.3.16 invariant: 2.2.4 - react: 18.2.0 + react: 18.3.1 react-lifecycles-compat: 3.0.4 - dev: false - /unicode-canonical-property-names-ecmascript/2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} + uncontrollable@8.0.4(react@18.3.1): + dependencies: + react: 18.3.1 - /unicode-match-property-ecmascript/2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} + underscore@1.12.1: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-canonical-property-names-ecmascript: 2.0.1 unicode-property-aliases-ecmascript: 2.1.0 - /unicode-match-property-value-ecmascript/2.0.0: - resolution: {integrity: sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==} - engines: {node: '>=4'} + unicode-match-property-value-ecmascript@2.2.0: {} - /unicode-property-aliases-ecmascript/2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} + unicode-property-aliases-ecmascript@2.1.0: {} - /unique-string/2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 - /universalify/0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} + universalify@0.2.0: {} - /universalify/2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} + universalify@2.0.1: {} - /unpipe/1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} + unpipe@1.0.0: {} - /unquote/1.1.1: - resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + unquote@1.1.1: {} - /upath/1.2.0: - resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} - engines: {node: '>=4'} + upath@1.2.0: {} - /update-browserslist-db/1.0.9_browserslist@4.21.4: - resolution: {integrity: sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: - browserslist: 4.21.4 - escalade: 3.1.1 - picocolors: 1.0.0 + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 - /uri-js/4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-js@4.4.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 - /urix/0.1.0: - resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} - deprecated: Please see https://github.com/lydell/urix#deprecated - dev: false + urix@0.1.0: {} - /url-parse/1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-parse@1.5.10: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - /use-sync-external-store/1.2.0_react@18.2.0: - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.2.2(react@18.3.1): dependencies: - react: 18.2.0 - dev: false + react: 18.3.1 + optional: true - /util-deprecate/1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util-deprecate@1.0.2: {} - /util.promisify/1.0.1: - resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==} + util.promisify@1.0.1: dependencies: - define-properties: 1.1.4 - es-abstract: 1.20.2 - has-symbols: 1.0.3 - object.getownpropertydescriptors: 2.1.4 - - /utila/0.4.0: - resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + define-properties: 1.2.1 + es-abstract: 1.23.5 + has-symbols: 1.1.0 + object.getownpropertydescriptors: 2.1.8 - /utils-merge/1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + utila@0.4.0: {} - /uuid/3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - dev: true + utils-merge@1.0.1: {} - /uuid/8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true + uuid@8.3.2: {} - /v8-compile-cache-lib/3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true + v8-compile-cache-lib@3.0.1: {} - /v8-to-istanbul/8.1.1: - resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} - engines: {node: '>=10.12.0'} + v8-to-istanbul@8.1.1: dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.8.0 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 1.9.0 source-map: 0.7.4 - /validate-npm-package-license/3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validate-npm-package-license@3.0.4: dependencies: - spdx-correct: 3.1.1 + spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true - /vary/1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + vary@1.1.2: {} - /void-elements/3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - dev: false + void-elements@3.1.0: {} - /w3c-hr-time/1.0.2: - resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + w3c-hr-time@1.0.2: dependencies: browser-process-hrtime: 1.0.0 - /w3c-xmlserializer/2.0.0: - resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} - engines: {node: '>=10'} + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@2.0.0: dependencies: xml-name-validator: 3.0.0 - /walker/1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + walker@1.0.8: dependencies: makeerror: 1.0.12 - /warning/4.0.3: - resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + warning@4.0.3: dependencies: loose-envify: 1.4.0 - dev: false - /watchpack/2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 - /wbuf/1.7.3: - resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + wbuf@1.7.3: dependencies: minimalistic-assert: 1.0.1 - /wcwidth/1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.3 - dev: true - - /web-vitals/2.1.4: - resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} - dev: true - - /webidl-conversions/3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false + webidl-conversions@4.0.2: {} - /webidl-conversions/4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - - /webidl-conversions/5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} + webidl-conversions@5.0.0: {} - /webidl-conversions/6.1.0: - resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} - engines: {node: '>=10.4'} + webidl-conversions@6.1.0: {} - /webpack-dev-middleware/5.3.3_webpack@5.74.0: - resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 + webpack-dev-middleware@5.3.4(webpack@5.97.1): dependencies: - colorette: 2.0.19 - memfs: 3.4.7 + colorette: 2.0.20 + memfs: 3.5.3 mime-types: 2.1.35 range-parser: 1.2.1 - schema-utils: 4.0.0 - webpack: 5.74.0 - - /webpack-dev-server/4.11.1_webpack@5.74.0: - resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} - engines: {node: '>= 12.13.0'} - hasBin: true - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - dependencies: - '@types/bonjour': 3.5.10 - '@types/connect-history-api-fallback': 1.3.5 - '@types/express': 4.17.14 - '@types/serve-index': 1.9.1 - '@types/serve-static': 1.15.0 - '@types/sockjs': 0.3.33 - '@types/ws': 8.5.3 + schema-utils: 4.2.0 + webpack: 5.97.1 + + webpack-dev-server@4.15.2(webpack@5.97.1): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.21 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.7 + '@types/sockjs': 0.3.36 + '@types/ws': 8.5.13 ansi-html-community: 0.0.8 - bonjour-service: 1.0.14 - chokidar: 3.5.3 - colorette: 2.0.19 - compression: 1.7.4 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.7.5 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.18.1 - graceful-fs: 4.2.10 - html-entities: 2.3.3 - http-proxy-middleware: 2.0.6_@types+express@4.17.14 - ipaddr.js: 2.0.1 - open: 8.4.0 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.5.2 + http-proxy-middleware: 2.0.7(@types/express@4.17.21) + ipaddr.js: 2.2.0 + launch-editor: 2.9.1 + open: 8.4.2 p-retry: 4.6.2 rimraf: 3.0.2 - schema-utils: 4.0.0 - selfsigned: 2.1.1 + schema-utils: 4.2.0 + selfsigned: 2.4.1 serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.74.0 - webpack-dev-middleware: 5.3.3_webpack@5.74.0 - ws: 8.8.1 + webpack-dev-middleware: 5.3.4(webpack@5.97.1) + ws: 8.18.0 + optionalDependencies: + webpack: 5.97.1 transitivePeerDependencies: - bufferutil - debug - supports-color - utf-8-validate - /webpack-manifest-plugin/4.1.1_webpack@5.74.0: - resolution: {integrity: sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==} - engines: {node: '>=12.22.0'} - peerDependencies: - webpack: ^4.44.2 || ^5.47.0 + webpack-manifest-plugin@4.1.1(webpack@5.97.1): dependencies: tapable: 2.2.1 - webpack: 5.74.0 + webpack: 5.97.1 webpack-sources: 2.3.1 - /webpack-sources/1.4.3: - resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} + webpack-sources@1.4.3: dependencies: source-list-map: 2.0.1 source-map: 0.6.1 - /webpack-sources/2.3.1: - resolution: {integrity: sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==} - engines: {node: '>=10.13.0'} + webpack-sources@2.3.1: dependencies: source-list-map: 2.0.1 source-map: 0.6.1 - /webpack-sources/3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} + webpack-sources@3.2.3: {} - /webpack/5.74.0: - resolution: {integrity: sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true + webpack@5.97.1: dependencies: - '@types/eslint-scope': 3.7.4 - '@types/estree': 0.0.51 - '@webassemblyjs/ast': 1.11.1 - '@webassemblyjs/wasm-edit': 1.11.1 - '@webassemblyjs/wasm-parser': 1.11.1 - acorn: 8.8.0 - acorn-import-assertions: 1.8.0_acorn@8.8.0 - browserslist: 4.21.4 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.10.0 - es-module-lexer: 0.9.3 + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 json-parse-even-better-errors: 2.3.1 loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.1.1 + schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.6_webpack@5.74.0 - watchpack: 2.4.0 + terser-webpack-plugin: 5.3.10(webpack@5.97.1) + watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - /websocket-driver/0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} + websocket-driver@0.7.4: dependencies: http-parser-js: 0.5.8 safe-buffer: 5.2.1 websocket-extensions: 0.1.4 - /websocket-extensions/0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} + websocket-extensions@0.1.4: {} - /whatwg-encoding/1.0.5: - resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + whatwg-encoding@1.0.5: dependencies: iconv-lite: 0.4.24 - /whatwg-fetch/3.6.2: - resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} - - /whatwg-mimetype/2.3.0: - resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + whatwg-fetch@3.6.20: {} - /whatwg-url/5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false + whatwg-mimetype@2.3.0: {} - /whatwg-url/7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 tr46: 1.0.1 webidl-conversions: 4.0.2 - /whatwg-url/8.7.0: - resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} - engines: {node: '>=10'} + whatwg-url@8.7.0: dependencies: lodash: 4.17.21 tr46: 2.1.0 webidl-conversions: 6.1.0 - /which-boxed-primitive/1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + which-boxed-primitive@1.1.0: dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 + is-bigint: 1.1.0 + is-boolean-object: 1.2.0 + is-number-object: 1.1.0 + is-string: 1.1.0 + is-symbol: 1.1.0 - /which/1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true + which-builtin-type@1.2.0: dependencies: - isexe: 2.0.0 + call-bind: 1.0.8 + function.prototype.name: 1.1.6 + has-tostringtag: 1.0.2 + is-async-function: 2.0.0 + is-date-object: 1.0.5 + is-finalizationregistry: 1.1.0 + is-generator-function: 1.0.10 + is-regex: 1.2.0 + is-weakref: 1.0.2 + isarray: 2.0.5 + which-boxed-primitive: 1.1.0 + which-collection: 1.0.2 + which-typed-array: 1.1.16 - /which/2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.3 + + which-module@2.0.1: {} + + which-typed-array@1.1.16: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: dependencies: isexe: 2.0.0 - /word-wrap/1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} + which@2.0.2: + dependencies: + isexe: 2.0.0 - /wordwrap/1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true + word-wrap@1.2.5: {} - /workbox-background-sync/6.5.4: - resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==} + workbox-background-sync@6.6.0: dependencies: - idb: 7.0.2 - workbox-core: 6.5.4 + idb: 7.1.1 + workbox-core: 6.6.0 - /workbox-broadcast-update/6.5.4: - resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==} + workbox-broadcast-update@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-build/6.5.4: - resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==} - engines: {node: '>=10.0.0'} + workbox-build@6.6.0(@types/babel__core@7.20.5): dependencies: - '@apideck/better-ajv-errors': 0.3.6_ajv@8.11.0 - '@babel/core': 7.19.1 - '@babel/preset-env': 7.19.1_@babel+core@7.19.1 - '@babel/runtime': 7.19.0 - '@rollup/plugin-babel': 5.3.1_qjhfxcwn2glzcb5646tzyg45bq - '@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.0 - '@rollup/plugin-replace': 2.4.2_rollup@2.79.0 + '@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1) + '@babel/core': 7.26.0 + '@babel/preset-env': 7.26.0(@babel/core@7.26.0) + '@babel/runtime': 7.26.0 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(@types/babel__core@7.20.5)(rollup@2.79.2) + '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.2) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) '@surma/rollup-plugin-off-main-thread': 2.2.3 - ajv: 8.11.0 + ajv: 8.17.1 common-tags: 1.8.2 fast-json-stable-stringify: 2.1.0 fs-extra: 9.1.0 glob: 7.2.3 lodash: 4.17.21 pretty-bytes: 5.6.0 - rollup: 2.79.0 - rollup-plugin-terser: 7.0.2_rollup@2.79.0 + rollup: 2.79.2 + rollup-plugin-terser: 7.0.2(rollup@2.79.2) source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 tempy: 0.6.0 upath: 1.2.0 - workbox-background-sync: 6.5.4 - workbox-broadcast-update: 6.5.4 - workbox-cacheable-response: 6.5.4 - workbox-core: 6.5.4 - workbox-expiration: 6.5.4 - workbox-google-analytics: 6.5.4 - workbox-navigation-preload: 6.5.4 - workbox-precaching: 6.5.4 - workbox-range-requests: 6.5.4 - workbox-recipes: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 - workbox-streams: 6.5.4 - workbox-sw: 6.5.4 - workbox-window: 6.5.4 + workbox-background-sync: 6.6.0 + workbox-broadcast-update: 6.6.0 + workbox-cacheable-response: 6.6.0 + workbox-core: 6.6.0 + workbox-expiration: 6.6.0 + workbox-google-analytics: 6.6.0 + workbox-navigation-preload: 6.6.0 + workbox-precaching: 6.6.0 + workbox-range-requests: 6.6.0 + workbox-recipes: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 + workbox-streams: 6.6.0 + workbox-sw: 6.6.0 + workbox-window: 6.6.0 transitivePeerDependencies: - '@types/babel__core' - supports-color - /workbox-cacheable-response/6.5.4: - resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==} + workbox-cacheable-response@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-core/6.5.4: - resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==} + workbox-core@6.6.0: {} - /workbox-expiration/6.5.4: - resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==} + workbox-expiration@6.6.0: dependencies: - idb: 7.0.2 - workbox-core: 6.5.4 + idb: 7.1.1 + workbox-core: 6.6.0 - /workbox-google-analytics/6.5.4: - resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==} + workbox-google-analytics@6.6.0: dependencies: - workbox-background-sync: 6.5.4 - workbox-core: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 + workbox-background-sync: 6.6.0 + workbox-core: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 - /workbox-navigation-preload/6.5.4: - resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==} + workbox-navigation-preload@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-precaching/6.5.4: - resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==} + workbox-precaching@6.6.0: dependencies: - workbox-core: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 + workbox-core: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 - /workbox-range-requests/6.5.4: - resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==} + workbox-range-requests@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-recipes/6.5.4: - resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==} + workbox-recipes@6.6.0: dependencies: - workbox-cacheable-response: 6.5.4 - workbox-core: 6.5.4 - workbox-expiration: 6.5.4 - workbox-precaching: 6.5.4 - workbox-routing: 6.5.4 - workbox-strategies: 6.5.4 + workbox-cacheable-response: 6.6.0 + workbox-core: 6.6.0 + workbox-expiration: 6.6.0 + workbox-precaching: 6.6.0 + workbox-routing: 6.6.0 + workbox-strategies: 6.6.0 - /workbox-routing/6.5.4: - resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==} + workbox-routing@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-strategies/6.5.4: - resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==} + workbox-strategies@6.6.0: dependencies: - workbox-core: 6.5.4 + workbox-core: 6.6.0 - /workbox-streams/6.5.4: - resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==} + workbox-streams@6.6.0: dependencies: - workbox-core: 6.5.4 - workbox-routing: 6.5.4 + workbox-core: 6.6.0 + workbox-routing: 6.6.0 - /workbox-sw/6.5.4: - resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} + workbox-sw@6.6.0: {} - /workbox-webpack-plugin/6.5.4_webpack@5.74.0: - resolution: {integrity: sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==} - engines: {node: '>=10.0.0'} - peerDependencies: - webpack: ^4.4.0 || ^5.9.0 + workbox-webpack-plugin@6.6.0(@types/babel__core@7.20.5)(webpack@5.97.1): dependencies: fast-json-stable-stringify: 2.1.0 pretty-bytes: 5.6.0 upath: 1.2.0 - webpack: 5.74.0 + webpack: 5.97.1 webpack-sources: 1.4.3 - workbox-build: 6.5.4 + workbox-build: 6.6.0(@types/babel__core@7.20.5) transitivePeerDependencies: - '@types/babel__core' - supports-color - /workbox-window/6.5.4: - resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==} + workbox-window@6.6.0: dependencies: - '@types/trusted-types': 2.0.2 - workbox-core: 6.5.4 + '@types/trusted-types': 2.0.7 + workbox-core: 6.6.0 - /wrap-ansi/6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /wrap-ansi/7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - /wrappy/1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 - /write-file-atomic/3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: dependencies: imurmurhash: 0.1.4 is-typedarray: 1.0.0 signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - /ws/7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + ws@7.5.10: {} - /ws/8.8.1: - resolution: {integrity: sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true + ws@8.18.0: {} - /xml-name-validator/3.0.0: - resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + xml-name-validator@3.0.0: {} - /xmlchars/2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmlchars@2.2.0: {} - /xtend/4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} + y18n@4.0.3: {} - /y18n/5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + y18n@5.0.8: {} - /yallist/4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@3.1.1: {} - /yaml/1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} + yallist@4.0.0: {} - /yaml/2.1.1: - resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==} - engines: {node: '>= 14'} - dev: true + yaml-loader@0.8.1: + dependencies: + javascript-stringify: 2.1.0 + loader-utils: 2.0.4 + yaml: 2.6.1 - /yargs-parser/20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} + yaml@1.10.2: {} - /yargs-parser/21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yaml@2.6.1: {} - /yargs/16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + yaml@2.7.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: dependencies: cliui: 7.0.4 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - /yargs/17.5.1: - resolution: {integrity: sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: - cliui: 7.0.4 - escalade: 3.1.1 + cliui: 8.0.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true - /yn/3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - dev: true + yn@3.1.1: {} - /yocto-queue/0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + yocto-queue@0.1.0: {} - /zustand/4.1.1_react@18.2.0: - resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} - engines: {node: '>=12.7.0'} - peerDependencies: - immer: '>=9.0' - react: '>=16.8' - peerDependenciesMeta: - immer: - optional: true - react: - optional: true - dependencies: - react: 18.2.0 - use-sync-external-store: 1.2.0_react@18.2.0 - dev: false + zustand@5.0.2(@types/react@18.3.16)(immer@9.0.21)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.16 + immer: 9.0.21 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml new file mode 100644 index 000000000..7e7ed713c --- /dev/null +++ b/ui/pnpm-workspace.yaml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +packages: + - "src/plugins/**" + - "!src/plugins/builtin/**" diff --git a/ui/public/index.html b/ui/public/index.html index efa315fc5..5bca47e40 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -1,39 +1,176 @@ + - + - - - - - + + + - - Answer -
- + + diff --git a/ui/public/manifest.json b/ui/public/manifest.json index 48b5bdfc5..a92240fba 100644 --- a/ui/public/manifest.json +++ b/ui/public/manifest.json @@ -1,6 +1,6 @@ { "short_name": "Answer", - "name": "Answer.dev", + "name": "Apache Answer", "icons": [ { "src": "favicon.ico", diff --git a/ui/public/robots.txt b/ui/public/robots.txt index e9e57dc4d..ca36cd253 100644 --- a/ui/public/robots.txt +++ b/ui/public/robots.txt @@ -1,3 +1,22 @@ +==== + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +==== + # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: diff --git a/ui/scripts/env.js b/ui/scripts/env.js new file mode 100644 index 000000000..8219ed4db --- /dev/null +++ b/ui/scripts/env.js @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const configFilePath = path.resolve(__dirname, '../../configs/config.yaml'); +const envFilePath = path.resolve(__dirname, '../.env.production'); + +// Read config.yaml file +const config = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + +// Generate .env file content +let envContent = 'TSC_COMPILE_ON_ERROR=true\nESLINT_NO_DEV_ERRORS=true\n'; +for (const key in config.ui) { + const value = config.ui[key]; + envContent += `${key !== 'public_url' ? 'REACT_APP_' : ''}${key.toUpperCase()}=${value}\n`; +} + +// Write .env file +fs.writeFileSync(envFilePath, envContent); diff --git a/ui/scripts/importPlugins.js b/ui/scripts/importPlugins.js new file mode 100644 index 000000000..0862a0158 --- /dev/null +++ b/ui/scripts/importPlugins.js @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); +const fs = require('fs'); + +const pluginPath = path.join(__dirname, '../src/plugins'); +const pluginFolders = fs.readdirSync(pluginPath); + +function resetPackageJson() { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJsonContent = require(packageJsonPath); + const dependencies = packageJsonContent.dependencies; + for (const key in dependencies) { + if (dependencies[key].startsWith('workspace')) { + delete dependencies[key]; + } + } + fs.writeFileSync( + packageJsonPath, + JSON.stringify(packageJsonContent, null, 2), + ); +} + +function addPluginToPackageJson(packageName) { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJsonContent = require(packageJsonPath); + packageJsonContent.dependencies[packageName] = 'workspace:*'; + + fs.writeFileSync( + packageJsonPath, + JSON.stringify(packageJsonContent, null, 2), + ); +} + + +resetPackageJson(); + +pluginFolders.forEach((folder) => { + const pluginFolder = path.join(pluginPath, folder); + const stat = fs.statSync(pluginFolder); + + if (stat.isDirectory() && folder !== 'builtin') { + if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) { + return; + } + const packageJson = require(path.join(pluginFolder, 'package.json')); + const packageName = packageJson.name; + + addPluginToPackageJson(packageName); + } +}); \ No newline at end of file diff --git a/ui/scripts/loadPlugins.js b/ui/scripts/loadPlugins.js new file mode 100644 index 000000000..77b5fe5df --- /dev/null +++ b/ui/scripts/loadPlugins.js @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +const pluginPath = path.join(__dirname, '../src/plugins'); +const pluginFolders = fs.readdirSync(pluginPath); + +function pascalize(str) { + return str.split(/[_-]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); +} + +function resetIndexTs() { + const indexTsPath = path.join(pluginPath, 'index.ts'); + fs.writeFileSync(indexTsPath, ''); +} + +function addPluginToIndexTs(packageName, pluginFolder) { + const indexTsPath = path.join(pluginPath, 'index.ts'); + const indexTsContent = fs.readFileSync(indexTsPath, 'utf-8'); + const lines = indexTsContent.split('\n'); + const ComponentName = pascalize(packageName); + + const importLine = `const load${ComponentName} = () => import('${packageName}').then(module => module.default);`; + const info = yaml.load(fs.readFileSync(path.join(pluginFolder, 'info.yaml'), 'utf8')); + const exportLine = `export const ${info.slug_name} = load${ComponentName}`; + + if (!lines.includes(exportLine)) { + lines.push(importLine); + lines.push(exportLine); + } + + fs.writeFileSync(indexTsPath, lines.join('\n')); +} + +const pluginLength = pluginFolders.filter((folder) => { + const pluginFolder = path.join(pluginPath, folder); + const stat = fs.statSync(pluginFolder); + return stat.isDirectory() && folder !== 'builtin'; +}).length; + +if (pluginLength > 0) { + resetIndexTs(); +} + +pluginFolders.forEach((folder) => { + const pluginFolder = path.join(pluginPath, folder); + const stat = fs.statSync(pluginFolder); + + if (stat.isDirectory() && folder !== 'builtin') { + if (!fs.existsSync(path.join(pluginFolder, 'index.ts'))) { + return; + } + const packageJson = require(path.join(pluginFolder, 'package.json')); + const packageName = packageJson.name; + + addPluginToIndexTs(packageName, pluginFolder); + } +}); + diff --git a/ui/scripts/preinstall.js b/ui/scripts/preinstall.js new file mode 100644 index 000000000..94de3f325 --- /dev/null +++ b/ui/scripts/preinstall.js @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed. + +require('./loadPlugins'); + +if (!/pnpm/.test(process.env.npm_execpath)) { + console.warn( + `\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`, + ); + process.exit(1); +} diff --git a/ui/scripts/setup-eslint.js b/ui/scripts/setup-eslint.js new file mode 100644 index 000000000..d9496933a --- /dev/null +++ b/ui/scripts/setup-eslint.js @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const UI_DIR = path.resolve(__dirname, '..'); // UI directory +const ROOT_DIR = path.resolve(UI_DIR, '..'); // Project root directory +const GIT_DIR = getGitDir(ROOT_DIR); // Git root directory +const HUSKY_DIR = path.join(GIT_DIR, '.husky'); + +// Find Git directory +function getGitDir(startDir) { + let currentDir = startDir; + + while (currentDir !== path.parse(currentDir).root) { + const gitDir = path.join(currentDir, '.git'); + if (fs.existsSync(gitDir)) { + return currentDir; + } + currentDir = path.dirname(currentDir); + } + + throw new Error('Could not find Git directory'); +} + + +if (!fs.existsSync(HUSKY_DIR)) { + console.log(`Creating husky directory: ${HUSKY_DIR}`); + fs.mkdirSync(HUSKY_DIR, { recursive: true }); +} + +if (!fs.existsSync(path.join(HUSKY_DIR, '_'))) { + console.log(`Creating husky _ directory: ${path.join(HUSKY_DIR, '_')}`); + fs.mkdirSync(path.join(HUSKY_DIR, '_'), { recursive: true }); +} + +// init husky +try { + console.log('Initializing husky...'); + execSync('npx husky install', { cwd: GIT_DIR, stdio: 'inherit' }); +} catch (error) { + console.error(`❌ Failed to initialize husky: ${error.message}`); + process.exit(1); +} + +// create lint-staged config file +const lintStagedConfig = { + "src/**/*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "src/**/*.{scss,md}": [ + "prettier --write" + ] +}; + +console.log(`Creating lint-staged config: ${path.join(UI_DIR, '.lintstagedrc.json')}`); +fs.writeFileSync( + path.join(UI_DIR, '.lintstagedrc.json'), + JSON.stringify(lintStagedConfig, null, 2) +); + +// create pre-commit hook +const preCommitContent = `#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +echo "🔍 Start running the code check..." + +# Getting the Git Root Directory +GIT_ROOT=$(git rev-parse --show-toplevel) + +# Get a list of staging files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) + +# Check for files in the ui/ directory +UI_FILES=$(echo "$STAGED_FILES" | grep '^ui/' || echo "") + +if [ -n "$UI_FILES" ]; then + echo "🔎 Discover ui file changes, run code checks..." + + # Switch to the ui directory + cd "$GIT_ROOT/ui" || { + echo "❌ Unable to access the UI catalog" + exit 1 + } + + # 运行 lint-staged + echo "✨ Running ESLint and Prettier Formatting..." + npx lint-staged --verbose + + LINT_STAGED_RESULT=$? + if [ $LINT_STAGED_RESULT -ne 0 ]; then + echo "❌ Code check failed, please fix the above problem" + exit $LINT_STAGED_RESULT + fi + + echo "✅ Code check passes!" +else + echo "ℹ️ No front-end file changes found, skip code checking" +fi + +echo "🎉 Pre-submission check completed" +`; + +console.log(`Creating pre-commit hook: ${path.join(HUSKY_DIR, 'pre-commit')}`); +fs.writeFileSync(path.join(HUSKY_DIR, 'pre-commit'), preCommitContent); +execSync(`chmod +x ${path.join(HUSKY_DIR, 'pre-commit')}`); + +// create husky.sh +const huskyShContent = `#!/bin/sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY=0, skip hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + export readonly husky_skip_init=1 + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + exit $exitCode +fi +`; + +console.log(`Creating husky.sh: ${path.join(HUSKY_DIR, '_', 'husky.sh')}`); +fs.writeFileSync( + path.join(HUSKY_DIR, '_', 'husky.sh'), + huskyShContent +); +execSync(`chmod +x ${path.join(HUSKY_DIR, '_', 'husky.sh')}`); + +console.log('Lint setup complete! Husky and lint-staged have been configured.'); +console.log(`Git root directory: ${GIT_DIR}`); +console.log(`Husky directory: ${HUSKY_DIR}`); diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index 74d156c56..7ea60b635 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + import React from 'react'; import { render, screen } from '@testing-library/react'; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d4f69259..8c425f293 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,8 +1,38 @@ -import { RouterProvider } from 'react-router-dom'; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ -import router from '@/router'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; + +import './i18n/init'; + +import '@/utils/pluginKit'; +import { useMergeRoutes } from '@/router'; +import InitialLoadingPlaceholder from '@/components/InitialLoadingPlaceholder'; function App() { + const routes = useMergeRoutes(); + if (routes.length === 0) { + return ; + } + const router = createBrowserRouter(routes, { + basename: process.env.REACT_APP_BASE_URL, + }); return ; } diff --git a/ui/src/assets/images/carousel-wecom-1.jpg b/ui/src/assets/images/carousel-wecom-1.jpg new file mode 100644 index 000000000..4dac62921 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-1.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-2.jpg b/ui/src/assets/images/carousel-wecom-2.jpg new file mode 100644 index 000000000..618db5af6 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-2.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-3.jpg b/ui/src/assets/images/carousel-wecom-3.jpg new file mode 100644 index 000000000..c8c7abb41 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-3.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-4.jpg b/ui/src/assets/images/carousel-wecom-4.jpg new file mode 100644 index 000000000..7f581fb49 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-4.jpg differ diff --git a/ui/src/assets/images/carousel-wecom-5.jpg b/ui/src/assets/images/carousel-wecom-5.jpg new file mode 100644 index 000000000..e4068ebe7 Binary files /dev/null and b/ui/src/assets/images/carousel-wecom-5.jpg differ diff --git a/ui/src/assets/images/default-avatar.svg b/ui/src/assets/images/default-avatar.svg index 08e0a60bc..7ecd67fc9 100644 --- a/ui/src/assets/images/default-avatar.svg +++ b/ui/src/assets/images/default-avatar.svg @@ -1,3 +1,21 @@ + diff --git a/ui/src/behaviour/useLegalClick.tsx b/ui/src/behaviour/useLegalClick.tsx new file mode 100644 index 000000000..599c9957c --- /dev/null +++ b/ui/src/behaviour/useLegalClick.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MouseEvent, useCallback } from 'react'; + +import { useLegalPrivacy, useLegalTos } from '@/services/client/legal'; + +export const useLegalClick = () => { + const { data: tos } = useLegalTos(); + const { data: privacy } = useLegalPrivacy(); + + const legalClick = useCallback( + (evt: MouseEvent, type: 'tos' | 'privacy') => { + evt.stopPropagation(); + const contentText = + type === 'tos' + ? tos?.terms_of_service_original_text + : privacy?.privacy_policy_original_text; + let matchUrl: URL | undefined; + try { + if (contentText) { + matchUrl = new URL(contentText); + } + // eslint-disable-next-line no-empty + } catch (ex) {} + if (matchUrl) { + evt.preventDefault(); + window.open(matchUrl.toString()); + } + }, + [tos, privacy], + ); + + return legalClick; +}; diff --git a/ui/src/common/_variable.scss b/ui/src/common/_variable.scss index 5687c2e4b..341705b6f 100644 --- a/ui/src/common/_variable.scss +++ b/ui/src/common/_variable.scss @@ -1,3 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + $link-hover-decoration: none; $enable-negative-margins: true; -$blue: #0033FF !default; +$blue: #0033ff !default; +$placeholder-opacity-max: 0.2; +$placeholder-opacity-min: 0.1; +$enable-smooth-scroll: false; diff --git a/ui/src/common/color.scss b/ui/src/common/color.scss new file mode 100644 index 000000000..1ded9c42c --- /dev/null +++ b/ui/src/common/color.scss @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +:root { + --an-side-nav-link: rgba(0, 0, 0, 0.65); + --an-toolbar-divider: rgba(0, 0, 0, 0.1); + --an-ced4da: #ced4da; + --an-e9ecef: #e9ecef; + --an-pre: #161b22; + --an-6c757d: #6c757d; + --an-212529: #212529; + --an-gray-300: var(--bs-gray-300); + --an-white: #fff; + --an-inbox-warning: #fff3cd80; + --an-f5: #f5f5f5; + --an-answer-item-border-top: rgba(0, 0, 0, 0.125); + --an-answer-inbox-nav-border-top: var(--bs-border-color); + --an-comment-item-border-bottom: var(--bs-colors-gray-200, #e9ecef); + --an-editor-toolbar-hover: #f8f9fa; + --ans-editor-toolbar-focus: #dae0e5; + --an-editor-placeholder-color: #6c757d; + --an-side-nav-link-hover-color: rgba(0, 0, 0, 0.85); + --an-invite-answer-item-active: #e9ecef; + --an-alert-exist-color: #055160; +} + +[data-bs-theme='dark'] { + --an-side-nav-link: rgba(255, 255, 255, 0.65); + --an-toolbar-divider: rgba(255, 255, 255, 0.3); + --an-ced4da: var(--bs-border-color); + --an-e9ecef: #161b22; + --an-pre: #161b22; + --an-6c757d: var(--bs-body-color); + --an-212529: var(--bs-body-color); + --an-gray-300: #161b22; + --an-white: #000; + --an-inbox-warning: #38363180; + --an-f5: var(--bs-body-bg); + --an-answer-item-border-top: var(--bs-border-color); + --an-answer-inbox-nav-border-top: var(--bs-border-color); + --an-comment-item-border-bottom: var(--bs-border-color); + --an-editor-toolbar-hover: var(--bs-tertiary-bg); + --ans-editor-toolbar-focus: var(--bs-tertiary-bg); + --an-editor-placeholder-color: var(--bs-body-color); + --an-side-nav-link-hover-color: var(--bs-body-color); + --an-invite-answer-item-active: var(--bs-tertiary-bg); + --an-alert-exist-color: #60cee4; +} + +[data-bs-theme='dark'] { + .link-dark { + color: rgba(var(--bs-emphasis-color-rgb), 0.8) !important; + } + /** CodeMirror **/ + + .cm-editor { + background: var(--bs-body-bg); + color: var(--bs-body-color); + } + + .cm-cursor { + border-left-color: var(--bs-body-color); + } + .ͼ2.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground { + background-color: #3e4451; + } + /** link color **/ + .ͼc { + color: rgb(60, 138, 233); + } + .ͼ5 { + color: var(--bs-body-color); + } + + .ͼ2 .cm-selectionBackground { + background: #3e4451; + } + /** CodeMirror end **/ + + .bg-light { + background-color: rgba(0, 0, 0, 0.5) !important; + } + .text-bg-dark { + color: #000 !important; + background-color: RGBA(255, 255, 255, var(--bs-bg-opacity, 1)) !important; + } + /** side nav **/ + #sideNav, + #answerAccordion { + .nav-link:hover, + .nav-link.active { + background-color: #2b3035 !important; + } + } + + /** tag **/ + .badge-tag { + background: $gray-800; + color: $gray-300; + &:hover { + background: $gray-600; + } + } + + .badge-tag-reserved { + color: $orange-200; + background: $orange-800; + &:hover { + background: $orange-700; + } + } + + .view-level1 { + color: $orange-300; + } + + .view-level2 { + color: $orange-200; + } + + .view-level3 { + color: $orange-100; + } +} diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 3e5775151..18f251145 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,58 +1,714 @@ -export const LOGIN_NEED_BACK = [ - '/users/login', - '/users/register', - '/users/account-recovery', - '/users/password-reset', -]; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DEFAULT_SITE_NAME = 'Answer'; +export const DEFAULT_LANG = 'en_US'; +export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; +export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_'; +export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; +export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; +export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; +export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_'; +export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_'; +export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; +export const DEFAULT_THEME = 'system'; +export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99; +export const SKELETON_SHOW_TIME = 1000; +export const LIST_VIEW_STORAGE_KEY = '_a_list_view_'; +export const EXTERNAL_CONTENT_DISPLAY_MODE = '_a_ecd_'; + +export const USER_AGENT_NAMES = { + SegmentFault: 'SegmentFault', + WeChat: 'WeChat', + WeCom: 'WeCom', + DingTalk: 'DingTalk', +}; export const ADMIN_LIST_STATUS = { // normal; 1: { - variant: 'success', + variant: 'text-bg-success', name: 'normal', }, // closed; 2: { - variant: 'warning', + variant: 'text-bg-warning', name: 'closed', }, // deleted 10: { - variant: 'danger', + variant: 'text-bg-danger', name: 'deleted', }, + // pending + 11: { + variant: 'text-bg-warning', + name: 'pending', + }, normal: { - variant: 'success', + variant: 'text-bg-success', name: 'normal', }, closed: { - variant: 'warning', + variant: 'text-bg-warning', name: 'closed', }, deleted: { - variant: 'danger', + variant: 'text-bg-danger', name: 'deleted', }, + pending: { + variant: 'text-bg-warning', + name: 'pending', + }, + unlisted: { + variant: 'text-bg-secondary', + name: 'unlisted', + }, }; export const ADMIN_NAV_MENUS = [ { name: 'dashboard', + icon: 'speedometer', children: [], }, { name: 'contents', - child: [{ name: 'questions' }, { name: 'answers' }], + icon: 'file-earmark-text-fill', + children: [{ name: 'questions' }, { name: 'answers' }], }, { name: 'users', + icon: 'people-fill', + }, + { + name: 'badges', + icon: 'award-fill', }, { - name: 'flags', - // badgeContent: 5, + name: 'apperance', + icon: 'palette-fill', + children: [ + { + name: 'themes', + }, + { + name: 'customize', + }, + { name: 'branding' }, + ], }, { name: 'settings', - child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], + icon: 'gear-fill', + children: [ + { name: 'general' }, + { name: 'interface' }, + { name: 'smtp' }, + { name: 'legal' }, + { name: 'write' }, + { name: 'seo' }, + { name: 'login' }, + { name: 'privileges' }, + ], + }, + { + name: 'plugins', + icon: 'plugin', + children: [ + { + name: 'installed_plugins', + path: 'installed-plugins', + }, + ], + }, +]; + +export const TIMEZONES = [ + { + label: 'Africa', + options: [ + { value: 'Africa/Abidjan', label: 'Abidjan' }, + { value: 'Africa/Accra', label: 'Accra' }, + { value: 'Africa/Addis_Ababa', label: 'Addis Ababa' }, + { value: 'Africa/Algiers', label: 'Algiers' }, + { value: 'Africa/Asmara', label: 'Asmara' }, + { value: 'Africa/Bamako', label: 'Bamako' }, + { value: 'Africa/Bangui', label: 'Bangui' }, + { value: 'Africa/Banjul', label: 'Banjul' }, + { value: 'Africa/Bissau', label: 'Bissau' }, + { value: 'Africa/Blantyre', label: 'Blantyre' }, + { value: 'Africa/Brazzaville', label: 'Brazzaville' }, + { value: 'Africa/Bujumbura', label: 'Bujumbura' }, + { value: 'Africa/Cairo', label: 'Cairo' }, + { value: 'Africa/Casablanca', label: 'Casablanca' }, + { value: 'Africa/Ceuta', label: 'Ceuta' }, + { value: 'Africa/Conakry', label: 'Conakry' }, + { value: 'Africa/Dakar', label: 'Dakar' }, + { value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' }, + { value: 'Africa/Djibouti', label: 'Djibouti' }, + { value: 'Africa/Douala', label: 'Douala' }, + { value: 'Africa/El_Aaiun', label: 'El Aaiun' }, + { value: 'Africa/Freetown', label: 'Freetown' }, + { value: 'Africa/Gaborone', label: 'Gaborone' }, + { value: 'Africa/Harare', label: 'Harare' }, + { value: 'Africa/Johannesburg', label: 'Johannesburg' }, + { value: 'Africa/Juba', label: 'Juba' }, + { value: 'Africa/Kampala', label: 'Kampala' }, + { value: 'Africa/Khartoum', label: 'Khartoum' }, + { value: 'Africa/Kigali', label: 'Kigali' }, + { value: 'Africa/Kinshasa', label: 'Kinshasa' }, + { value: 'Africa/Lagos', label: 'Lagos' }, + { value: 'Africa/Libreville', label: 'Libreville' }, + { value: 'Africa/Lome', label: 'Lome' }, + { value: 'Africa/Luanda', label: 'Luanda' }, + { value: 'Africa/Lubumbashi', label: 'Lubumbashi' }, + { value: 'Africa/Lusaka', label: 'Lusaka' }, + { value: 'Africa/Malabo', label: 'Malabo' }, + { value: 'Africa/Maputo', label: 'Maputo' }, + { value: 'Africa/Maseru', label: 'Maseru' }, + { value: 'Africa/Mbabane', label: 'Mbabane' }, + { value: 'Africa/Mogadishu', label: 'Mogadishu' }, + { value: 'Africa/Monrovia', label: 'Monrovia' }, + { value: 'Africa/Nairobi', label: 'Nairobi' }, + { value: 'Africa/Ndjamena', label: 'Ndjamena' }, + { value: 'Africa/Niamey', label: 'Niamey' }, + { value: 'Africa/Nouakchott', label: 'Nouakchott' }, + { value: 'Africa/Ouagadougou', label: 'Ouagadougou' }, + { value: 'Africa/Porto-Novo', label: 'Porto-Novo' }, + { value: 'Africa/Sao_Tome', label: 'Sao Tome' }, + { value: 'Africa/Tripoli', label: 'Tripoli' }, + { value: 'Africa/Tunis', label: 'Tunis' }, + { value: 'Africa/Windhoek', label: 'Windhoek' }, + ], + }, + { + label: 'America', + options: [ + { value: 'America/Adak', label: 'Adak' }, + { value: 'America/Anchorage', label: 'Anchorage' }, + { value: 'America/Anguilla', label: 'Anguilla' }, + { value: 'America/Antigua', label: 'Antigua' }, + { value: 'America/Araguaina', label: 'Araguaina' }, + { + value: 'America/Argentina/Buenos_Aires', + label: 'Argentina - Buenos Aires', + }, + { value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' }, + { value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' }, + { value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' }, + { value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' }, + { value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' }, + { + value: 'America/Argentina/Rio_Gallegos', + label: 'Argentina - Rio Gallegos', + }, + { value: 'America/Argentina/Salta', label: 'Argentina - Salta' }, + { value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' }, + { value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' }, + { value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' }, + { value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' }, + { value: 'America/Aruba', label: 'Aruba' }, + { value: 'America/Asuncion', label: 'Asuncion' }, + { value: 'America/Atikokan', label: 'Atikokan' }, + { value: 'America/Bahia', label: 'Bahia' }, + { value: 'America/Bahia_Banderas', label: 'Bahia Banderas' }, + { value: 'America/Barbados', label: 'Barbados' }, + { value: 'America/Belem', label: 'Belem' }, + { value: 'America/Belize', label: 'Belize' }, + { value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' }, + { value: 'America/Boa_Vista', label: 'Boa Vista' }, + { value: 'America/Bogota', label: 'Bogota' }, + { value: 'America/Boise', label: 'Boise' }, + { value: 'America/Cambridge_Bay', label: 'Cambridge Bay' }, + { value: 'America/Campo_Grande', label: 'Campo Grande' }, + { value: 'America/Cancun', label: 'Cancun' }, + { value: 'America/Caracas', label: 'Caracas' }, + { value: 'America/Cayenne', label: 'Cayenne' }, + { value: 'America/Cayman', label: 'Cayman' }, + { value: 'America/Chicago', label: 'Chicago' }, + { value: 'America/Chihuahua', label: 'Chihuahua' }, + { value: 'America/Costa_Rica', label: 'Costa Rica' }, + { value: 'America/Creston', label: 'Creston' }, + { value: 'America/Cuiaba', label: 'Cuiaba' }, + { value: 'America/Curacao', label: 'Curacao' }, + { value: 'America/Danmarkshavn', label: 'Danmarkshavn' }, + { value: 'America/Dawson', label: 'Dawson' }, + { value: 'America/Dawson_Creek', label: 'Dawson Creek' }, + { value: 'America/Denver', label: 'Denver' }, + { value: 'America/Detroit', label: 'Detroit' }, + { value: 'America/Dominica', label: 'Dominica' }, + { value: 'America/Edmonton', label: 'Edmonton' }, + { value: 'America/Eirunepe', label: 'Eirunepe' }, + { value: 'America/El_Salvador', label: 'El Salvador' }, + { value: 'America/Fort_Nelson', label: 'Fort Nelson' }, + { value: 'America/Fortaleza', label: 'Fortaleza' }, + { value: 'America/Glace_Bay', label: 'Glace Bay' }, + { value: 'America/Godthab', label: 'Godthab' }, + { value: 'America/Goose_Bay', label: 'Goose Bay' }, + { value: 'America/Grand_Turk', label: 'Grand Turk' }, + { value: 'America/Grenada', label: 'Grenada' }, + { value: 'America/Guadeloupe', label: 'Guadeloupe' }, + { value: 'America/Guatemala', label: 'Guatemala' }, + { value: 'America/Guayaquil', label: 'Guayaquil' }, + { value: 'America/Guyana', label: 'Guyana' }, + { value: 'America/Halifax', label: 'Halifax' }, + { value: 'America/Havana', label: 'Havana' }, + { value: 'America/Hermosillo', label: 'Hermosillo' }, + { + value: 'America/Indiana/Indianapolis', + label: 'Indiana - Indianapolis', + }, + { value: 'America/Indiana/Knox', label: 'Indiana - Knox' }, + { value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' }, + { value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' }, + { value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' }, + { value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' }, + { value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' }, + { value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' }, + { value: 'America/Inuvik', label: 'Inuvik' }, + { value: 'America/Iqaluit', label: 'Iqaluit' }, + { value: 'America/Jamaica', label: 'Jamaica' }, + { value: 'America/Juneau', label: 'Juneau' }, + { value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' }, + { value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' }, + { value: 'America/Kralendijk', label: 'Kralendijk' }, + { value: 'America/La_Paz', label: 'La Paz' }, + { value: 'America/Lima', label: 'Lima' }, + { value: 'America/Los_Angeles', label: 'Los Angeles' }, + { value: 'America/Lower_Princes', label: 'Lower Princes' }, + { value: 'America/Maceio', label: 'Maceio' }, + { value: 'America/Managua', label: 'Managua' }, + { value: 'America/Manaus', label: 'Manaus' }, + { value: 'America/Marigot', label: 'Marigot' }, + { value: 'America/Martinique', label: 'Martinique' }, + { value: 'America/Matamoros', label: 'Matamoros' }, + { value: 'America/Mazatlan', label: 'Mazatlan' }, + { value: 'America/Miquelon', label: 'Miquelon' }, + { value: 'America/Moncton', label: 'Moncton' }, + { value: 'America/Monterrey', label: 'Monterrey' }, + { value: 'America/Montevideo', label: 'Montevideo' }, + { value: 'America/Montserrat', label: 'Montserrat' }, + { value: 'America/Nassau', label: 'Nassau' }, + { value: 'America/New_York', label: 'New York' }, + { value: 'America/Nipigon', label: 'Nipigon' }, + { value: 'America/Nome', label: 'Nome' }, + { value: 'America/Noronha', label: 'Noronha' }, + { value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' }, + { value: 'America/North_Dakota/Center', label: 'North Dakota - Center' }, + { + value: 'America/North_Dakota/New_Salem', + label: 'North Dakota - New Salem', + }, + { value: 'America/Ojinaga', label: 'Ojinaga' }, + { value: 'America/Panama', label: 'Panama' }, + { value: 'America/Pangnirtung', label: 'Pangnirtung' }, + { value: 'America/Paramaribo', label: 'Paramaribo' }, + { value: 'America/Phoenix', label: 'Phoenix' }, + { value: 'America/Port-au-Prince', label: 'Port-au-Prince' }, + { value: 'America/Port_of_Spain', label: 'Port of Spain' }, + { value: 'America/Porto_Velho', label: 'Porto Velho' }, + { value: 'America/Puerto_Rico', label: 'Puerto Rico' }, + { value: 'America/Punta_Arenas', label: 'Punta Arenas' }, + { value: 'America/Rainy_River', label: 'Rainy River' }, + { value: 'America/Rankin_Inlet', label: 'Rankin Inlet' }, + { value: 'America/Recife', label: 'Recife' }, + { value: 'America/Regina', label: 'Regina' }, + { value: 'America/Resolute', label: 'Resolute' }, + { value: 'America/Rio_Branco', label: 'Rio Branco' }, + { value: 'America/Santarem', label: 'Santarem' }, + { value: 'America/Santiago', label: 'Santiago' }, + { value: 'America/Santo_Domingo', label: 'Santo Domingo' }, + { value: 'America/Sao_Paulo', label: 'Sao Paulo' }, + { value: 'America/Scoresbysund', label: 'Scoresbysund' }, + { value: 'America/Sitka', label: 'Sitka' }, + { value: 'America/St_Barthelemy', label: 'St Barthelemy' }, + { value: 'America/St_Johns', label: 'St Johns' }, + { value: 'America/St_Kitts', label: 'St Kitts' }, + { value: 'America/St_Lucia', label: 'St Lucia' }, + { value: 'America/St_Thomas', label: 'St Thomas' }, + { value: 'America/St_Vincent', label: 'St Vincent' }, + { value: 'America/Swift_Current', label: 'Swift Current' }, + { value: 'America/Tegucigalpa', label: 'Tegucigalpa' }, + { value: 'America/Thule', label: 'Thule' }, + { value: 'America/Thunder_Bay', label: 'Thunder Bay' }, + { value: 'America/Tijuana', label: 'Tijuana' }, + { value: 'America/Toronto', label: 'Toronto' }, + { value: 'America/Tortola', label: 'Tortola' }, + { value: 'America/Vancouver', label: 'Vancouver' }, + { value: 'America/Whitehorse', label: 'Whitehorse' }, + { value: 'America/Winnipeg', label: 'Winnipeg' }, + { value: 'America/Yakutat', label: 'Yakutat' }, + { value: 'America/Yellowknife', label: 'Yellowknife' }, + ], + }, + { + label: 'Antarctica', + options: [ + { value: 'Antarctica/Casey', label: 'Casey' }, + { value: 'Antarctica/Davis', label: 'Davis' }, + { value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' }, + { value: 'Antarctica/Macquarie', label: 'Macquarie' }, + { value: 'Antarctica/Mawson', label: 'Mawson' }, + { value: 'Antarctica/McMurdo', label: 'McMurdo' }, + { value: 'Antarctica/Palmer', label: 'Palmer' }, + { value: 'Antarctica/Rothera', label: 'Rothera' }, + { value: 'Antarctica/Syowa', label: 'Syowa' }, + { value: 'Antarctica/Troll', label: 'Troll' }, + { value: 'Antarctica/Vostok', label: 'Vostok' }, + ], + }, + { + label: 'Arctic', + options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }], + }, + { + label: 'Asia', + options: [ + { value: 'Asia/Aden', label: 'Aden' }, + { value: 'Asia/Almaty', label: 'Almaty' }, + { value: 'Asia/Amman', label: 'Amman' }, + { value: 'Asia/Anadyr', label: 'Anadyr' }, + { value: 'Asia/Aqtau', label: 'Aqtau' }, + { value: 'Asia/Aqtobe', label: 'Aqtobe' }, + { value: 'Asia/Ashgabat', label: 'Ashgabat' }, + { value: 'Asia/Atyrau', label: 'Atyrau' }, + { value: 'Asia/Baghdad', label: 'Baghdad' }, + { value: 'Asia/Bahrain', label: 'Bahrain' }, + { value: 'Asia/Baku', label: 'Baku' }, + { value: 'Asia/Bangkok', label: 'Bangkok' }, + { value: 'Asia/Barnaul', label: 'Barnaul' }, + { value: 'Asia/Beirut', label: 'Beirut' }, + { value: 'Asia/Bishkek', label: 'Bishkek' }, + { value: 'Asia/Brunei', label: 'Brunei' }, + { value: 'Asia/Chita', label: 'Chita' }, + { value: 'Asia/Choibalsan', label: 'Choibalsan' }, + { value: 'Asia/Colombo', label: 'Colombo' }, + { value: 'Asia/Damascus', label: 'Damascus' }, + { value: 'Asia/Dhaka', label: 'Dhaka' }, + { value: 'Asia/Dili', label: 'Dili' }, + { value: 'Asia/Dubai', label: 'Dubai' }, + { value: 'Asia/Dushanbe', label: 'Dushanbe' }, + { value: 'Asia/Famagusta', label: 'Famagusta' }, + { value: 'Asia/Gaza', label: 'Gaza' }, + { value: 'Asia/Hebron', label: 'Hebron' }, + { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' }, + { value: 'Asia/Hong_Kong', label: 'Hong Kong' }, + { value: 'Asia/Hovd', label: 'Hovd' }, + { value: 'Asia/Irkutsk', label: 'Irkutsk' }, + { value: 'Asia/Jakarta', label: 'Jakarta' }, + { value: 'Asia/Jayapura', label: 'Jayapura' }, + { value: 'Asia/Jerusalem', label: 'Jerusalem' }, + { value: 'Asia/Kabul', label: 'Kabul' }, + { value: 'Asia/Kamchatka', label: 'Kamchatka' }, + { value: 'Asia/Karachi', label: 'Karachi' }, + { value: 'Asia/Kathmandu', label: 'Kathmandu' }, + { value: 'Asia/Khandyga', label: 'Khandyga' }, + { value: 'Asia/Kolkata', label: 'Kolkata' }, + { value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' }, + { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' }, + { value: 'Asia/Kuching', label: 'Kuching' }, + { value: 'Asia/Kuwait', label: 'Kuwait' }, + { value: 'Asia/Macau', label: 'Macau' }, + { value: 'Asia/Magadan', label: 'Magadan' }, + { value: 'Asia/Makassar', label: 'Makassar' }, + { value: 'Asia/Manila', label: 'Manila' }, + { value: 'Asia/Muscat', label: 'Muscat' }, + { value: 'Asia/Nicosia', label: 'Nicosia' }, + { value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' }, + { value: 'Asia/Novosibirsk', label: 'Novosibirsk' }, + { value: 'Asia/Omsk', label: 'Omsk' }, + { value: 'Asia/Oral', label: 'Oral' }, + { value: 'Asia/Phnom_Penh', label: 'Phnom Penh' }, + { value: 'Asia/Pontianak', label: 'Pontianak' }, + { value: 'Asia/Pyongyang', label: 'Pyongyang' }, + { value: 'Asia/Qatar', label: 'Qatar' }, + { value: 'Asia/Qostanay', label: 'Qostanay' }, + { value: 'Asia/Qyzylorda', label: 'Qyzylorda' }, + { value: 'Asia/Riyadh', label: 'Riyadh' }, + { value: 'Asia/Sakhalin', label: 'Sakhalin' }, + { value: 'Asia/Samarkand', label: 'Samarkand' }, + { value: 'Asia/Seoul', label: 'Seoul' }, + { value: 'Asia/Shanghai', label: 'Shanghai' }, + { value: 'Asia/Singapore', label: 'Singapore' }, + { value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' }, + { value: 'Asia/Taipei', label: 'Taipei' }, + { value: 'Asia/Tashkent', label: 'Tashkent' }, + { value: 'Asia/Tbilisi', label: 'Tbilisi' }, + { value: 'Asia/Tehran', label: 'Tehran' }, + { value: 'Asia/Thimphu', label: 'Thimphu' }, + { value: 'Asia/Tokyo', label: 'Tokyo' }, + { value: 'Asia/Tomsk', label: 'Tomsk' }, + { value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' }, + { value: 'Asia/Urumqi', label: 'Urumqi' }, + { value: 'Asia/Ust-Nera', label: 'Ust-Nera' }, + { value: 'Asia/Vientiane', label: 'Vientiane' }, + { value: 'Asia/Vladivostok', label: 'Vladivostok' }, + { value: 'Asia/Yakutsk', label: 'Yakutsk' }, + { value: 'Asia/Yangon', label: 'Yangon' }, + { value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' }, + { value: 'Asia/Yerevan', label: 'Yerevan' }, + ], + }, + { + label: 'Atlantic', + options: [ + { value: 'Atlantic/Azores', label: 'Azores' }, + { value: 'Atlantic/Bermuda', label: 'Bermuda' }, + { value: 'Atlantic/Canary', label: 'Canary' }, + { value: 'Atlantic/Cape_Verde', label: 'Cape Verde' }, + { value: 'Atlantic/Faroe', label: 'Faroe' }, + { value: 'Atlantic/Madeira', label: 'Madeira' }, + { value: 'Atlantic/Reykjavik', label: 'Reykjavik' }, + { value: 'Atlantic/South_Georgia', label: 'South Georgia' }, + { value: 'Atlantic/Stanley', label: 'Stanley' }, + { value: 'Atlantic/St_Helena', label: 'St Helena' }, + ], + }, + { + label: 'Australia', + options: [ + { value: 'Australia/Adelaide', label: 'Adelaide' }, + { value: 'Australia/Brisbane', label: 'Brisbane' }, + { value: 'Australia/Broken_Hill', label: 'Broken Hill' }, + { value: 'Australia/Currie', label: 'Currie' }, + { value: 'Australia/Darwin', label: 'Darwin' }, + { value: 'Australia/Eucla', label: 'Eucla' }, + { value: 'Australia/Hobart', label: 'Hobart' }, + { value: 'Australia/Lindeman', label: 'Lindeman' }, + { value: 'Australia/Lord_Howe', label: 'Lord Howe' }, + { value: 'Australia/Melbourne', label: 'Melbourne' }, + { value: 'Australia/Perth', label: 'Perth' }, + { value: 'Australia/Sydney', label: 'Sydney' }, + ], + }, + { + label: 'Europe', + options: [ + { value: 'Europe/Amsterdam', label: 'Amsterdam' }, + { value: 'Europe/Andorra', label: 'Andorra' }, + { value: 'Europe/Astrakhan', label: 'Astrakhan' }, + { value: 'Europe/Athens', label: 'Athens' }, + { value: 'Europe/Belgrade', label: 'Belgrade' }, + { value: 'Europe/Berlin', label: 'Berlin' }, + { value: 'Europe/Bratislava', label: 'Bratislava' }, + { value: 'Europe/Brussels', label: 'Brussels' }, + { value: 'Europe/Bucharest', label: 'Bucharest' }, + { value: 'Europe/Budapest', label: 'Budapest' }, + { value: 'Europe/Busingen', label: 'Busingen' }, + { value: 'Europe/Chisinau', label: 'Chisinau' }, + { value: 'Europe/Copenhagen', label: 'Copenhagen' }, + { value: 'Europe/Dublin', label: 'Dublin' }, + { value: 'Europe/Gibraltar', label: 'Gibraltar' }, + { value: 'Europe/Guernsey', label: 'Guernsey' }, + { value: 'Europe/Helsinki', label: 'Helsinki' }, + { value: 'Europe/Isle_of_Man', label: 'Isle of Man' }, + { value: 'Europe/Istanbul', label: 'Istanbul' }, + { value: 'Europe/Jersey', label: 'Jersey' }, + { value: 'Europe/Kaliningrad', label: 'Kaliningrad' }, + { value: 'Europe/Kiev', label: 'Kiev' }, + { value: 'Europe/Kirov', label: 'Kirov' }, + { value: 'Europe/Lisbon', label: 'Lisbon' }, + { value: 'Europe/Ljubljana', label: 'Ljubljana' }, + { value: 'Europe/London', label: 'London' }, + { value: 'Europe/Luxembourg', label: 'Luxembourg' }, + { value: 'Europe/Madrid', label: 'Madrid' }, + { value: 'Europe/Malta', label: 'Malta' }, + { value: 'Europe/Mariehamn', label: 'Mariehamn' }, + { value: 'Europe/Minsk', label: 'Minsk' }, + { value: 'Europe/Monaco', label: 'Monaco' }, + { value: 'Europe/Moscow', label: 'Moscow' }, + { value: 'Europe/Oslo', label: 'Oslo' }, + { value: 'Europe/Paris', label: 'Paris' }, + { value: 'Europe/Podgorica', label: 'Podgorica' }, + { value: 'Europe/Prague', label: 'Prague' }, + { value: 'Europe/Riga', label: 'Riga' }, + { value: 'Europe/Rome', label: 'Rome' }, + { value: 'Europe/Samara', label: 'Samara' }, + { value: 'Europe/San_Marino', label: 'San Marino' }, + { value: 'Europe/Sarajevo', label: 'Sarajevo' }, + { value: 'Europe/Saratov', label: 'Saratov' }, + { value: 'Europe/Simferopol', label: 'Simferopol' }, + { value: 'Europe/Skopje', label: 'Skopje' }, + { value: 'Europe/Sofia', label: 'Sofia' }, + { value: 'Europe/Stockholm', label: 'Stockholm' }, + { value: 'Europe/Tallinn', label: 'Tallinn' }, + { value: 'Europe/Tirane', label: 'Tirane' }, + { value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' }, + { value: 'Europe/Uzhgorod', label: 'Uzhgorod' }, + { value: 'Europe/Vaduz', label: 'Vaduz' }, + { value: 'Europe/Vatican', label: 'Vatican' }, + { value: 'Europe/Vienna', label: 'Vienna' }, + { value: 'Europe/Vilnius', label: 'Vilnius' }, + { value: 'Europe/Volgograd', label: 'Volgograd' }, + { value: 'Europe/Warsaw', label: 'Warsaw' }, + { value: 'Europe/Zagreb', label: 'Zagreb' }, + { value: 'Europe/Zaporozhye', label: 'Zaporozhye' }, + { value: 'Europe/Zurich', label: 'Zurich' }, + ], + }, + { + label: 'Indian', + options: [ + { value: 'Indian/Antananarivo', label: 'Antananarivo' }, + { value: 'Indian/Chagos', label: 'Chagos' }, + { value: 'Indian/Christmas', label: 'Christmas' }, + { value: 'Indian/Cocos', label: 'Cocos' }, + { value: 'Indian/Comoro', label: 'Comoro' }, + { value: 'Indian/Kerguelen', label: 'Kerguelen' }, + { value: 'Indian/Mahe', label: 'Mahe' }, + { value: 'Indian/Maldives', label: 'Maldives' }, + { value: 'Indian/Mauritius', label: 'Mauritius' }, + { value: 'Indian/Mayotte', label: 'Mayotte' }, + { value: 'Indian/Reunion', label: 'Reunion' }, + ], + }, + { + label: 'Pacific', + options: [ + { value: 'Pacific/Apia', label: 'Apia' }, + { value: 'Pacific/Auckland', label: 'Auckland' }, + { value: 'Pacific/Bougainville', label: 'Bougainville' }, + { value: 'Pacific/Chatham', label: 'Chatham' }, + { value: 'Pacific/Chuuk', label: 'Chuuk' }, + { value: 'Pacific/Easter', label: 'Easter' }, + { value: 'Pacific/Efate', label: 'Efate' }, + { value: 'Pacific/Enderbury', label: 'Enderbury' }, + { value: 'Pacific/Fakaofo', label: 'Fakaofo' }, + { value: 'Pacific/Fiji', label: 'Fiji' }, + { value: 'Pacific/Funafuti', label: 'Funafuti' }, + + { value: 'Pacific/Galapagos', label: 'Galapagos' }, + { value: 'Pacific/Gambier', label: 'Gambier' }, + { value: 'Pacific/Guadalcanal', label: 'Guadalcanal' }, + { value: 'Pacific/Guam', label: 'Guam' }, + { value: 'Pacific/Honolulu', label: 'Honolulu' }, + { value: 'Pacific/Kiritimati', label: 'Kiritimati' }, + { value: 'Pacific/Kosrae', label: 'Kosrae' }, + { value: 'Pacific/Kwajalein', label: 'Kwajalein' }, + { value: 'Pacific/Majuro', label: 'Majuro' }, + { value: 'Pacific/Marquesas', label: 'Marquesas' }, + { value: 'Pacific/Midway', label: 'Midway' }, + { value: 'Pacific/Nauru', label: 'Nauru' }, + { value: 'Pacific/Niue', label: 'Niue' }, + { value: 'Pacific/Norfolk', label: 'Norfolk' }, + { value: 'Pacific/Noumea', label: 'Noumea' }, + { value: 'Pacific/Pago_Pago', label: 'Pago Pago' }, + { value: 'Pacific/Palau', label: 'Palau' }, + { value: 'Pacific/Pitcairn', label: 'Pitcairn' }, + { value: 'Pacific/Pohnpei', label: 'Pohnpei' }, + { value: 'Pacific/Port_Moresby', label: 'Port Moresby' }, + { value: 'Pacific/Rarotonga', label: 'Rarotonga' }, + { value: 'Pacific/Saipan', label: 'Saipan' }, + { value: 'Pacific/Tahiti', label: 'Tahiti' }, + { value: 'Pacific/Tarawa', label: 'Tarawa' }, + { value: 'Pacific/Tongatapu', label: 'Tongatapu' }, + { value: 'Pacific/Wake', label: 'Wake' }, + { value: 'Pacific/Wallis', label: 'Wallis' }, + ], + }, + + { + label: 'UTC', + options: [{ value: 'UTC', label: 'UTC' }], + }, +]; +export const DEFAULT_TIMEZONE = 'UTC'; + +export const TIMELINE_NORMAL_ACTIVITY_TYPE = [ + 'undeleted', + 'deleted', + 'downvote', + 'upvote', + 'reopened', + 'closed', + 'pin', + 'unpin', + 'show', + 'hide', +]; + +export const SYSTEM_AVATAR_OPTIONS = [ + { + label: 'System', + value: 'system', + }, + { + label: 'Gravatar', + value: 'gravatar', + }, +]; + +export const TAG_SLUG_NAME_MAX_LENGTH = 35; + +export const DEFAULT_THEME_COLOR = '#0033ff'; + +export const SUSPENSE_USER_TIME = [ + { + label: 'hours', + time: '24', + value: '24h', + }, + { + label: 'hours', + time: '48', + value: '48h', + }, + { + label: 'hours', + time: '72', + value: '72h', + }, + { + label: 'days', + time: '7', + value: '7d', + }, + { + label: 'days', + time: '14', + value: '14d', + }, + { + label: 'months', + time: '1', + value: '1m', + }, + { + label: 'months', + time: '2', + value: '2m', + }, + { + label: 'months', + time: '3', + value: '3m', + }, + { + label: 'months', + time: '6', + value: '6m', + }, + { + label: 'year', + time: '1', + value: '1y', }, ]; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 6558a97cc..114b0e4ce 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -1,13 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + export interface FormValue { value: T; isInvalid: boolean; errorMsg: string; + [prop: string]: any; } export interface FormDataType { [prop: string]: FormValue; } +export interface FieldError { + error_field: string; + error_msg: string; +} + export interface Paging { page: number; page_size?: number; @@ -23,12 +48,15 @@ export interface ReportParams { export interface TagBase { display_name: string; slug_name: string; + original_text?: string; + recommend?: boolean; + reserved?: boolean; } export interface Tag extends TagBase { main_tag_slug_name?: string; - original_text?: string; parsed_text?: string; + tag_id?: string; } export interface SynonymsTag extends Tag { @@ -48,24 +76,30 @@ export interface TagInfo extends TagBase { updated_at?; main_tag_slug_name?: string; excerpt?; + status: string; } -export interface QuestionParams { +export interface QuestionParams extends ImgCodeReq { title: string; + url_title?: string; content: string; - html: string; tags: Tag[]; } +export interface QuestionWithAnswer extends QuestionParams { + answer_content: string; +} + export interface ListResult { count: number; list: T[]; } -export interface AnswerParams { +export interface AnswerParams extends ImgCodeReq { content: string; html: string; question_id: string; id: string; + edit_summary?: string; } export interface LoginReqParams { @@ -89,40 +123,58 @@ export interface ModifyPasswordReq { export interface ModifyUserReq { display_name: string; username?: string; - avatar: string; + avatar: any; bio: string; bio_html?: string; location: string; website: string; } -export interface UserInfoBase { +enum RoleId { + User = 1, + Admin = 2, + Moderator = 3, +} + +export interface User { + username: string; + rank: number; + vote_count: number; + display_name: string; avatar: string; +} + +export interface UserInfoBase { + id?: string; + avatar: any; username: string; display_name: string; rank: number; website: string; location: string; ip_info?: string; - /** 'forbidden' | 'normal' | 'delete' - */ - status?: string; + status?: 'normal' | 'suspended' | 'deleted' | 'inactive'; /** roles */ - is_admin?: true; + role_id?: RoleId; } export interface UserInfoRes extends UserInfoBase { bio: string; bio_html: string; create_time?: string; - /** value = 1 active; value = 2 inactivated + /** + * value = 1 active; + * value = 2 inactivated */ mail_status: number; + language: string; e_mail?: string; + have_password: boolean; [prop: string]: any; } -export interface AvatarUploadReq { +export type UploadType = 'post' | 'avatar' | 'branding' | 'post_attachment'; +export interface UploadReq { file: FormData; } @@ -141,14 +193,48 @@ export interface PasswordResetReq extends ImgCodeReq { e_mail: string; } -export interface CheckImgReq { - action: 'login' | 'e_mail' | 'find_pass'; +export interface PasswordReplaceReq extends ImgCodeReq { + code: string; + pass: string; } +export interface CaptchaReq extends ImgCodeReq { + verify: ImgCodeRes['verify']; +} + +export type CaptchaKey = + | 'email' + | 'password' + | 'edit_userinfo' + | 'question' + | 'answer' + | 'comment' + | 'edit' + | 'invitation_answer' + | 'search' + | 'report' + | 'delete' + | 'vote'; + export interface SetNoticeReq { notice_switch: boolean; } +export interface NotificationBadgeAward { + notification_id: string; + badge_id: string; + name: string; + icon: string; + level: number; +} +export interface NotificationStatus { + inbox: number; + achievement: number; + revision: number; + can_revision: boolean; + badge_award: NotificationBadgeAward | null; +} + export interface QuestionDetailRes { id: string; title: string; @@ -167,12 +253,13 @@ export interface QuestionDetailRes { user_info: UserInfoBase; answered: boolean; collected: boolean; + answer_ids: string[]; [prop: string]: any; } export interface AnswersReq extends Paging { - order?: 'default' | 'updated'; + order?: 'default' | 'updated' | 'created'; question_id: string; } @@ -184,13 +271,12 @@ export interface AnswerItem { create_time: string; update_time: string; user_info: UserInfoBase; - [prop: string]: any; } -export interface PostAnswerReq { +export interface PostAnswerReq extends ImgCodeReq { content: string; - html: string; + html?: string; question_id: string; } @@ -210,23 +296,31 @@ export interface LangsType { * @description interface for Question */ export type QuestionOrderBy = + | 'recommend' | 'newest' | 'active' - | 'frequent' + | 'hot' | 'score' - | 'unanswered'; + | 'unanswered' + | 'frequent'; export interface QueryQuestionsReq extends Paging { order: QuestionOrderBy; - tags?: string[]; + tag?: string; + in_days?: number; } -export type AdminQuestionStatus = 'available' | 'closed' | 'deleted'; +export type AdminQuestionStatus = + | 'available' + | 'pending' + | 'closed' + | 'deleted'; -export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted'; +export type AdminContentsFilterBy = 'normal' | 'pending' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; + query?: string; } /** @@ -237,8 +331,20 @@ export type AdminAnswerStatus = 'available' | 'deleted'; /** * @description interface for Users */ -export type UserFilterBy = 'all' | 'inactive' | 'suspended' | 'deleted'; +export type UserFilterBy = + | 'normal' + | 'staff' + | 'inactive' + | 'suspended' + | 'deleted'; + +export type BadgeFilterBy = 'all' | 'active' | 'inactive'; +export type InstalledPluginsFilterBy = + | 'all' + | 'active' + | 'inactive' + | 'outdated'; /** * @description interface for Flags */ @@ -256,12 +362,28 @@ export interface AdminSettingsGeneral { name: string; short_description: string; description: string; + site_url: string; + contact_email: string; + check_update: boolean; + permalink?: number; +} + +export interface HelmetBase { + pageTitle?: string; + description?: string; + keywords?: string; +} + +export interface HelmetUpdate extends Omit { + title?: string; + subtitle?: string; } export interface AdminSettingsInterface { - logo: string; language: string; - theme: string; + time_zone?: string; + default_avatar: string; + gravatar_base_url: string; } export interface AdminSettingsSmtp { @@ -270,15 +392,99 @@ export interface AdminSettingsSmtp { from_name: string; smtp_authentication: boolean; smtp_host: string; - smtp_password: string; + smtp_password?: string; smtp_port: number; - smtp_username: string; + smtp_username?: string; test_email_recipient?: string; } +export interface AdminSettingsUsers { + allow_update_avatar: boolean; + allow_update_bio: boolean; + allow_update_display_name: boolean; + allow_update_location: boolean; + allow_update_username: boolean; + allow_update_website: boolean; +} + export interface SiteSettings { + branding: AdminSettingBranding; general: AdminSettingsGeneral; interface: AdminSettingsInterface; + login: AdminSettingsLogin; + custom_css_html: AdminSettingsCustom; + theme: AdminSettingsTheme; + site_seo: AdminSettingsSeo; + site_users: AdminSettingsUsers; + site_write: AdminSettingsWrite; + version: string; + revision: string; + site_legal: AdminSettingsLegal; +} + +export interface AdminSettingBranding { + logo: string; + square_icon: string; + mobile_logo?: string; + favicon?: string; +} + +export interface AdminSettingsLegal { + external_content_display: string; + privacy_policy_original_text?: string; + privacy_policy_parsed_text?: string; + terms_of_service_original_text?: string; + terms_of_service_parsed_text?: string; +} + +export interface AdminSettingsWrite { + restrict_answer?: boolean; + recommend_tags?: Tag[]; + required_tag?: boolean; + reserved_tags?: Tag[]; + max_image_size?: number; + max_attachment_size?: number; + max_image_megapixel?: number; + authorized_image_extensions?: string[]; + authorized_attachment_extensions?: string[]; +} + +export interface AdminSettingsSeo { + robots: string; + /** + * 0: not set + * 1:with title + * 2: no title + */ + permalink: number; +} + +export type themeConfig = { + navbar_style: string; + primary_color: string; + [k: string]: string | number; +}; +export interface AdminSettingsTheme { + theme: string; + color_scheme: string; + theme_options?: { label: string; value: string }[]; + theme_config: Record; +} + +export interface AdminSettingsCustom { + custom_css: string; + custom_head: string; + custom_header: string; + custom_footer: string; + custom_sidebar: string; +} + +export interface AdminSettingsLogin { + allow_new_registrations: boolean; + login_required: boolean; + allow_email_registrations: boolean; + allow_email_domains: string[]; + allow_password_login: boolean; } /** @@ -292,7 +498,7 @@ export interface FollowParams { /** * @description search request params */ -export interface SearchParams { +export interface SearchParams extends ImgCodeReq { q: string; order: string; page: number; @@ -305,7 +511,9 @@ export interface SearchParams { export interface SearchResItem { object_type: string; object: { + url_title?: string; id: string; + question_id?: string; title: string; excerpt: string; created_at: number; @@ -320,3 +528,281 @@ export interface SearchResItem { export interface SearchRes extends ListResult { extra: any; } + +export interface AdminDashboard { + info: { + question_count: number; + resolved_count: number; + resolved_rate: string; + unanswered_count: number; + unanswered_rate: string; + answer_count: number; + comment_count: number; + vote_count: number; + user_count: number; + report_count: number; + uploading_files: boolean; + smtp: 'enabled' | 'disabled' | 'not_configured'; + time_zone: string; + occupying_storage_space: string; + app_start_time: number; + https: boolean; + login_required: boolean; + go_version: string; + database_version: string; + database_size: string; + version_info: { + remote_version: string; + version: string; + }; + }; +} + +export interface TimelineReq { + show_vote: boolean; + object_id: string; +} + +export interface TimelineItem { + activity_id: number; + revision_id: number; + created_at: number; + activity_type: string; + comment: string; + object_id: string; + object_type: string; + cancelled: boolean; + cancelled_at: any; + user_info: UserInfoBase; +} + +export interface TimelineObject { + title: string; + url_title?: string; + object_type: string; + question_id: string; + answer_id: string; + main_tag_slug_name?: string; + display_name?: string; +} + +export interface TimelineRes { + object_info: TimelineObject; + timeline: TimelineItem[]; +} + +export interface SuggestReviewItem { + type: 'question' | 'answer' | 'tag'; + info: { + url_title?: string; + object_id: string; + title: string; + content: string; + html: string; + tags: Tag[]; + }; + unreviewed_info: { + id: string; + use_id: string; + object_id: string; + title: string; + status: 0 | 1; + create_at: number; + user_info: UserInfoBase; + reason: string; + content: Tag | QuestionDetailRes | AnswerItem; + }; +} +export interface SuggestReviewResp { + count: number; + list: SuggestReviewItem[]; +} + +export interface ReasonItem { + content_type: string; + description: string; + name: string; + placeholder: string; + reason_type: number; +} + +export interface BaseReviewItem { + object_type: 'question' | 'answer' | 'comment' | 'user'; + object_id: string; + object_show_status: number; + object_status: number; + tags: Tag[]; + title: string; + original_text: string; + author_user_info: UserInfoBase; + created_at: number; + submit_at: number; + comment_id: string; + question_id: string; + answer_id: string; + answer_count: number; + answer_accepted?: boolean; + flag_id: string; + url_title: string; + parsed_text: string; +} + +export interface FlagReviewItem extends BaseReviewItem { + reason: ReasonItem; + reason_content: string; + submitter_user: UserInfoBase; +} + +export interface FlagReviewResp { + count: number; + list: FlagReviewItem[]; +} + +export interface QueuedReviewItem extends BaseReviewItem { + review_id: number; + reason: string; + submitter_display_name: string; +} + +export interface QueuedReviewResp { + count: number; + list: QueuedReviewItem[]; +} + +export interface UserRoleItem { + id: number; + name: string; + description: string; +} +export interface MemberActionItem { + action: string; + name: string; + type: string; +} + +export interface QuestionOperationReq { + id: string; + operation: 'pin' | 'unpin' | 'hide' | 'show'; +} + +export interface OauthBindEmailReq { + binding_key: string; + email: string; + must: boolean; +} + +export interface UserOauthConnectorItem { + icon: string; + name: string; + link: string; + binding: boolean; + external_id: string; +} + +export interface NotificationConfigItem { + enable: boolean; + key: string; +} +export interface NotificationConfig { + all_new_question: NotificationConfigItem; + all_new_question_for_following_tags: NotificationConfigItem; + inbox: NotificationConfigItem; +} + +export interface ActivatedPlugin { + slug_name: string; + enabled: boolean; +} + +export interface UserPluginsConfigRes { + name: string; + slug_name: string; +} + +export interface ReviewTypeItem { + label: string; + name: string; + todo_amount: number; +} + +export interface PutFlagReviewParams { + operation_type: + | 'edit_post' + | 'close_post' + | 'delete_post' + | 'unlist_post' + | 'ignore_report'; + flag_id: string; + close_msg?: string; + close_type?: number; + title?: string; + content?: string; + tags?: Tag[]; + // mention_username_list?: any; + captcha_code?: any; + captcha_id?: any; +} + +/** + * @description response for reaction + */ +export interface ReactionItems { + reaction_summary: ReactionItem[]; +} + +export interface ReactionItem { + emoji: string; + count: number; + tooltip: string; + is_active: boolean; +} + +export interface BadgeListItem { + id: string; + name: string; + icon: string; + award_count: number; + earned: boolean; + /** 1: bronze 2: silver 3:gold */ + level: number; + earned_count?: number; +} + +export interface BadgeListGroupItem { + badges: BadgeListItem[]; + group_name: string; +} + +export interface BadgeInfo extends BadgeListItem { + description: string; + earned_count: number; + is_single: boolean; +} + +export interface AdminBadgeListItem extends BadgeListItem { + group_name: string; + status: string; + description: string; +} + +export interface BadgeDetailListReq { + page: number; + page_size: number; + badge_id: string; + username?: string | null; +} +export interface BadgeDetailListItem { + created_at: number; + author_user_info: UserInfoBase; + object_type: string; + object_id: string; + url_title: string; + question_id: string; + answer_id: string; + comment_id: string; +} + +export interface BadgeDetailListRes { + count: number; + list: BadgeDetailListItem[]; +} diff --git a/ui/src/common/pattern.ts b/ui/src/common/pattern.ts index 406e5e3e2..133228b18 100644 --- a/ui/src/common/pattern.ts +++ b/ui/src/common/pattern.ts @@ -1,6 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + const pattern = { email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, + search: + /(\[.*\])|(is:answer)|(is:question)|(score:\d*)|(user:\S*)|(answers:\d*)/g, + uaWeChat: /micromessenger/i, + uaWeCom: /wxwork/i, + uaDingTalk: /dingtalk/i, }; export default pattern; diff --git a/ui/src/common/sideNavLayout.scss b/ui/src/common/sideNavLayout.scss new file mode 100644 index 000000000..db2fd0163 --- /dev/null +++ b/ui/src/common/sideNavLayout.scss @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.answer-container { + width: 100%; + min-height: calc(100vh - 95px - 62px); + max-width: 1072px; +} + +.page-right-side { + flex: none; + width: 300px; + box-sizing: content-box; +} + +// lg +@media screen and (max-width: 1199.9px) { + .answer-container { + padding-left: 12px; + padding-right: 12px; + } + .page-main { + max-width: 100%; + } + .page-right-side { + width: 100%; + box-sizing: border-box; + } +} diff --git a/ui/src/components/AccordionNav/index.css b/ui/src/components/AccordionNav/index.css new file mode 100644 index 000000000..524cf0c99 --- /dev/null +++ b/ui/src/components/AccordionNav/index.css @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.collapse-indicator { + transition: all 0.2s ease; +} + +.expanding .collapse-indicator { + transform: rotate(90deg); +} + +#answerAccordion { + max-width: 208px; + .nav-link { + color: var(--an-side-nav-link); + } + .nav-link:focus-visible { + box-shadow: none; + } + .nav-link:hover { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-100); + } + .nav-link.active { + color: var(--an-side-nav-link-hover-color); + background-color: var(--bs-gray-200); + } +} diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index ccc14d446..ee0819787 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -1,109 +1,192 @@ -import React, { FC } from 'react'; -import { Accordion, Badge, Button, Stack } from 'react-bootstrap'; +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { Accordion, Nav } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useMatch } from 'react-router-dom'; +import { useNavigate, useMatch, NavLink } from 'react-router-dom'; -import { useAccordionButton } from 'react-bootstrap/AccordionButton'; +import classNames from 'classnames'; -import { Icon } from '@answer/components'; +import { floppyNavigation } from '@/utils'; +import { Icon } from '@/components'; +import './index.css'; -function MenuNode({ menu, callback, activeKey, isLeaf = false }) { - const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); - const accordionClick = useAccordionButton(menu.name); - const menuOnClick = (evt) => { - evt.preventDefault(); - evt.stopPropagation(); - if (!isLeaf) { - accordionClick(evt); - } - if (typeof callback === 'function') { - callback(menu); - } - }; +function MenuNode({ + menu, + callback, + activeKey, + expanding = false, + path = '/', +}) { + const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); + const isLeaf = !menu.children.length; + const href = isLeaf ? `${path}${menu.path}` : '#'; - let menuCls = 'text-start text-dark text-nowrap shadow-none bg-body border-0'; - let menuVariant = 'light'; - if (activeKey === menu.name) { - menuCls = 'text-start text-white text-nowrap shadow-none'; - menuVariant = 'primary'; - } return ( - + + {isLeaf ? ( + { + callback(evt, menu, href, isLeaf); + }} + className={classNames( + 'text-nowrap d-flex flex-nowrap align-items-center w-100', + { expanding, active: activeKey === menu.path }, + )}> + {menu?.icon && } + + + {menu.displayName ? menu.displayName : t(menu.name)} + + {menu.badgeContent ? ( + {menu.badgeContent} + ) : null} + {!isLeaf && ( + + )} + + ) : ( + { + callback(evt, menu, href, isLeaf); + }} + className={classNames( + 'text-nowrap d-flex flex-nowrap align-items-center w-100', + { expanding, active: activeKey === menu.path }, + )}> + {menu?.icon && } + + {menu.displayName ? menu.displayName : t(menu.name)} + + {menu.badgeContent ? ( + {menu.badgeContent} + ) : null} + {!isLeaf && ( + + )} + + )} + + {menu.children.length ? ( + + <> + {menu.children.map((leaf) => { + return ( + + ); + })} + + + ) : null} + ); } interface AccordionProps { menus: any[]; + path?: string; } -const AccordionNav: FC = ({ menus }) => { +const AccordionNav: FC = ({ menus = [], path = '/' }) => { const navigate = useNavigate(); - let activeKey = menus[0].name; - const pathMatch = useMatch('/admin/*'); + const pathMatch = useMatch(`${path}*`); + // auto set menu fields + menus.forEach((m) => { + if (!m.path) { + m.path = m.name; + } + if (!Array.isArray(m.children)) { + m.children = []; + } + m.children.forEach((sm) => { + if (!sm.path) { + sm.path = sm.name; + } + if (!Array.isArray(sm.children)) { + sm.children = []; + } + }); + }); + const splat = pathMatch && pathMatch.params['*']; + let activeKey = menus[0].path; if (splat) { activeKey = splat; } - const menuClick = (clickedMenu) => { - const menuKey = clickedMenu.name; - if (Array.isArray(clickedMenu.child) && clickedMenu.child.length) { - return; - } - if (activeKey !== menuKey) { - const routePath = `/admin/${menuKey}`; - navigate(routePath); - } + const getOpenKey = () => { + let openKey = ''; + menus.forEach((li) => { + if (li.children.length) { + const matchedChild = li.children.find((el) => { + return el.path === activeKey; + }); + if (matchedChild) { + openKey = li.path; + } + } + }); + return openKey; }; - let defaultOpenKey; - menus.forEach((li) => { - if (Array.isArray(li.child) && li.child.length) { - const matchedChild = li.child.find((el) => { - return el.name === activeKey; - }); - if (matchedChild) { - defaultOpenKey = li.name; + const [openKey, setOpenKey] = useState(getOpenKey()); + const menuClick = (evt, menu, href, isLeaf) => { + evt.stopPropagation(); + if (isLeaf) { + if (floppyNavigation.shouldProcessLinkClick(evt)) { + evt.preventDefault(); + navigate(href); } + } else { + setOpenKey(openKey === menu.path ? '' : menu.path); } - }); - + }; + useEffect(() => { + setOpenKey(getOpenKey()); + }, [activeKey, menus]); return ( - - + + ); }; diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 87c35cef2..b58d0c8cd 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -1,17 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + import { memo, FC, useState, useEffect } from 'react'; import { Button, ButtonGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { Icon } from '@answer/components'; -import { bookmark, postVote } from '@answer/api'; -import { isLogin } from '@answer/utils'; -import { userInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; +import { Icon } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; +import { tryNormalLogged } from '@/utils/guard'; +import { bookmark, postVote } from '@/services'; +import * as Types from '@/common/interface'; interface Props { className?: string; + source: 'question' | 'answer'; data: { id: string; votesCount: number; @@ -24,7 +46,7 @@ interface Props { }; } -const Index: FC = ({ className, data }) => { +const Index: FC = ({ className, data, source }) => { const [votes, setVotes] = useState(0); const [like, setLike] = useState(false); const [hate, setHated] = useState(false); @@ -32,9 +54,11 @@ const Index: FC = ({ className, data }) => { state: data?.collected, count: data?.collectCount, }); - const { username = '' } = userInfoStore((state) => state.user); + const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); + const vCaptcha = useCaptchaPlugin('vote'); + useEffect(() => { if (data) { setVotes(data.votesCount); @@ -47,32 +71,32 @@ const Index: FC = ({ className, data }) => { } }, []); - const handleVote = (type: 'up' | 'down') => { - if (!isLogin(true)) { - return; - } - - if (data.username === username) { - toast.onShow({ - msg: t('cannot_vote_for_self'), - variant: 'danger', - }); - return; - } + const submitVote = (type) => { const isCancel = (type === 'up' && like) || (type === 'down' && hate); + const imgCode: Types.ImgCodeReq = { + captcha_id: undefined, + captcha_code: undefined, + }; + vCaptcha?.resolveCaptchaReq?.(imgCode); + postVote( { object_id: data?.id, is_cancel: isCancel, + ...imgCode, }, type, ) - .then((res) => { + .then(async (res) => { + await vCaptcha?.close(); setVotes(res.votes); setLike(res.vote_status === 'vote_up'); setHated(res.vote_status === 'vote_down'); }) .catch((err) => { + if (err?.isError) { + vCaptcha?.handleCaptchaError(err.list); + } const errMsg = err?.value; if (errMsg) { toast.onShow({ @@ -83,16 +107,40 @@ const Index: FC = ({ className, data }) => { }); }; + const handleVote = (type: 'up' | 'down') => { + if (!tryNormalLogged(true)) { + return; + } + + if (data.username === username) { + toast.onShow({ + msg: t('cannot_vote_for_self'), + variant: 'danger', + }); + return; + } + + if (!vCaptcha) { + submitVote(type); + return; + } + + vCaptcha.check(() => { + submitVote(type); + }); + }; + const handleBookmark = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } bookmark({ group_id: '0', object_id: data?.id, + bookmark: !bookmarkState.state, }).then((res) => { setBookmark({ - state: res.switch, + state: !bookmarkState.state, count: res.object_collection_count, }); }); @@ -102,15 +150,25 @@ const Index: FC = ({ className, data }) => {
- + +
+ ); +}; + +export default Index; diff --git a/ui/src/components/CardBadge/index.scss b/ui/src/components/CardBadge/index.scss new file mode 100644 index 000000000..c3021a748 --- /dev/null +++ b/ui/src/components/CardBadge/index.scss @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.badge-card { + .label { + position: absolute; + top: 1rem; + right: 1rem; + } +} diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx new file mode 100644 index 000000000..3022ae125 --- /dev/null +++ b/ui/src/components/CardBadge/index.tsx @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useTranslation } from 'react-i18next'; +import { FC } from 'react'; +import { Card, Badge } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; + +import classnames from 'classnames'; + +import { Icon } from '@/components'; +import * as Type from '@/common/interface'; +import { formatCount } from '@/utils'; + +import './index.scss'; + +interface IProps { + data: Type.BadgeListItem; + showAwardedCount?: boolean; + urlSearchParams?: string; + badgePillType?: 'earned' | 'count'; +} + +const Index: FC = ({ + data, + badgePillType = 'earned', + showAwardedCount = false, + urlSearchParams, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + return ( + + + {Number(data?.earned_count) > 0 && badgePillType === 'earned' && ( + + {`${t('earned')}${ + Number(data?.earned_count) > 1 ? ` ×${data.earned_count}` : '' + }`} + + )} + + {badgePillType === 'count' && Number(data?.earned_count) > 1 && ( + + ×{data.earned_count} + + )} + {data.icon.startsWith('http') ? ( + {data.name} + ) : ( + + )} + +
{data.name}
+ {showAwardedCount && ( +
+ {t('×_awarded', { number: formatCount(data.award_count) })} +
+ )} +
+ + ); +}; + +export default Index; diff --git a/ui/src/components/Comment/components/ActionBar/index.tsx b/ui/src/components/Comment/components/ActionBar/index.tsx index a801a6bb6..c55bf40e2 100644 --- a/ui/src/components/Comment/components/ActionBar/index.tsx +++ b/ui/src/components/Comment/components/ActionBar/index.tsx @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + import { memo } from 'react'; import { Button, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; @@ -5,7 +24,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; -import { Icon, FormatTime } from '@answer/components'; +import { Icon, FormatTime } from '@/components'; const ActionBar = ({ nickName, @@ -22,19 +41,27 @@ const ActionBar = ({ const { t } = useTranslation('translation', { keyPrefix: 'comment' }); return ( -
-
+
+
{userStatus !== 'deleted' ? ( - {nickName} + + {nickName} + ) : ( {nickName} )} - +